February 18, 2020

Аппаратный CTF. Легкий способ узнать ключ шифрования, когда у тебя под рукой осциллограф и ноутбук

Источник: t.me/Bureau121

Содержание статьи

  • Современное железо
  • Суть атаки
  • Подготовка платы
  • Пишем прошивку
  • Зачем осциллографу Ethernet
  • Собираем все вместе
  • Предобработка
  • Кто такая Julia
  • Подводим итоги
  • Дополнительные материалы
  • Постскриптум

Современное железо

В первую очередь было решено выбрать в качестве цели более актуальное железо. Arduino и AVR — это прекрасно, это наше наследие, и к нему нужно относиться с подобающим уважением, а не ломать направо и налево. Но сегодня бал правят микроконтроллеры на ядре ARM (Cortex-M). И вероятнее всего, именно их ты обнаружишь, распотрошив ближайшее умное устройство — беспроводную розетку, лампочку с Wi-Fi и прочие прелести сомнительной автоматизации.

На первый взгляд, бюджетный микроконтроллер STM32F091 (Cortex-M0) на плате Nucleo-64 идеально подходил на роль подопытного кролика. По сравнению с Arduino Nano это решение выглядит выигрышно буквально по всем параметрам: разрядность 32 бит (против восьми), тактовая частота 48 МГц (против шестнадцати) и напряжение питания всего 3,3 В против пяти. Что касается цены, нужно учесть, что оригинальные итальянские платы Arduino стоят как минимум столько же, если не дороже.

Такой выбор на самом деле осложняет атаку — не всякий бюджетный осциллограф сможет угнаться за микроконтроллером на подобных скоростях. Использование Hantek DSO6022 из презентации было бы заранее обречено на провал, достаточно только взглянуть на его характеристики.

К счастью, у меня на столе стоял осциллограф RTC1002 компании Rohde & Schwarz. Если верить рекламным материалам, это модель начального уровня, но это обманчивое впечатление. Да, такой осциллограф стоит как флагманский смартфон, но у него масса полезных функций, и он максимально удобен.

Суть атаки

Остальные условия изначального конкурса я решил не менять. Мы по-прежнему будем анализировать трассы энергопотребления нашего устройства в момент шифрования данных и пытаться угадать хранящийся в памяти пароль. Шифр — AES в режиме ECB с длиной ключа в 128 бит.

Конечно, на практике в реальном устройстве ты, скорее всего, обнаружишь AES в режиме CBC, но я так и не придумал, как органично вписать вектор инициализации в условия задачи. Сам по себе IV без ключа шифрования не представляет особой ценности для злоумышленника и нередко передается в открытом виде (примером тому могут служить протоколы SSL/TLS, смотри RFC 5246).

Сам по себе алгоритм AES считается одним из лучших, используется практически повсеместно, и на сегодняшний день для него не известно ни одной реальной уязвимости. Что же может пойти не так и есть ли у нас вообще шансы на успех?

Все дело в том, что операции любого процессора на самом низком уровне сводятся к переключению транзисторов. На это расходуется энергия, потребление которой можно фиксировать. Зная алгоритм шифрования, входные данные и замеряя параметры питания, мы можем попытаться угадать двоичное представление ключа (количество транзисторов в активном состоянии). И это будет однозначно гораздо лучше, чем простой перебор! (Да что угодно лучше, чем простой перебор, если уж откровенно.)

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

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

Таким образом, принцип атаки следующий: мы посылаем с компьютера на вход исследуемого устройства случайный набор чисел, устройство их шифрует и отправляет обратно, а осциллограф внимательно следит за расходом электроэнергии (счетчикам ЖКХ такая точность и не снилась). После этого по команде с компьютера мы сохраняем осциллограмму и набор данных (вход и выход) и повторяем процесс.

В теории все выглядит соблазнительно просто, не так ли? Сколько раз я так ошибался.


Подготовка платы

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

Отламываем программатор ST-Link — широкие прорези в самой плате намекают на то, что он прекрасно будет работать и отдельно от основной части. Если опасаешься повредить плату и выдрать целый кусок, попробуй сперва подпилить соединения обычной ножовкой, сразу должно быть проще.

Теперь наступил черед конденсаторов в цепи питания. Микроконтроллер требует 3,3 В, но это совместимая с Arduino плата, и она рассчитана на внешний источник питания 5 В, за это отвечает линейный DC/DC-преобразователь LD39050PU33R. То, что он линейный, — это хорошо, от импульсного были бы лишние помехи.

Конденсаторы С20 и С21 на входе можно оставить, а вот С18 и С19 явно лишние, смело бери паяльник в руки и выпаивай. Да их тут и не было! Также совершенно необязательны конденсаторы С23, С24, С27 и C28. У них какие-то смешные емкости — 100 нФ — можно даже не обращать внимания. Понравилось? Все, стой, остальные пусть живут!

Теперь о грустном. Мы перед этим отломали программатор, но он нам все еще нужен. Ты ведь его не выбросил, верно? Это было очень предусмотрительно! И дело не в том, что программатор нам понадобится для того, чтобы прошить микроконтроллер. Нет, это, оказывается, еще и переходник с USB — UART, и с его помощью мы будем общаться с компьютером.

Проблема в том, что дорожки RX/TX интерфейса UART располагались как раз на перемычках, которые мы до этого перерезали. На программаторе эти сигналы доступны на разъеме CN3, но на самой плате с F091 контакты D0/D1 разъема CN9 не подсоединены к микроконтроллеру. За это отвечают перемычки SB62 и SB63 на обратной стороне платы, так что соедини их парой капель припоя.

Кстати, ты обратил внимание, что на плате отсутствует кварцевый резонатор на месте X3? Есть «часовой» кварц на 32,768 кГц на месте X2, но он предназначен для RTC. Оказывается, ST-Link, кроме того что выполняет свои основные функции, еще и генерирует опорную частоту в 8 МГц для основного микроконтроллера! Это линия МСО на схеме (вывод PF0).

В отличие от интерфейса UART, этот сигнал не выведен ни на какие контакты, и без него F091 работать точно откажется. Мне не пришло в голову ничего лучше, чем купить в ближайшем магазине радиодеталей кварцевый генератор на 8 МГц. Его следует подключить к линии 5 В. При этом меандр будет с амплитудой 5 В вместо положенных 3,3 В, но это не страшно, так как вывод PF0 обозначен в даташите на микроконтроллер как FT, то есть совместимый с пятивольтовой логикой.

Остается только отпаять перемычку SB50 и запаять SB55, чтобы вывести ножку PF0 на 29-й контакт разъема CN7. Вполне вероятно, сейчас тебя занимает только одна мысль: зачем разработчики в ST придумали такую сложную схему? Оказывается, на такой печатной плате основана вся модельная линейка отладок Nucleo-64, а это почти двадцать разных версий. Конкретная версия отличается сочетанием всех этих крошечных перемычек.

Пишем прошивку

Теперь нужна программная реализация алгоритма AES. Сотрудники Riscure после проведения конкурса RHme2 выложили исходники примеров на своем GitHub. Но воспользоваться ими не получится по той простой причине, что они платформенно зависимые. Таблицы подстановок шифра AES с помощью макросов pgm_read_byte программируются во флеш, который разворачивается в цепочку инструкций ассемблера для AVR.

Понятно, что для F091 на ARM такой код запустить не удастся. Поэтому я выбрал стороннюю реализацию AES в режиме ECB, дописав недостающие куски самостоятельно. В целом это была самая простая часть, логика программы полностью повторяет принцип работы оригинального примера из RHme2. Заранее сгенерированный случайным образом ключ AES будет храниться в самом устройстве:

/* Key is: 0x47f1ed9166c996b2f553b147be3fbc20 (128-bit) */
const uint8_t key[] = {0x47, 0xF1, 0xED, 0x91, 0x66, 0xC9, 0x96,
    0xB2, 0xF5, 0x53, 0xB1, 0x47, 0xBE, 0x3F, 0xBC, 0x20};

Зачем осциллографу Ethernet

Протестировав работоспособность платы и прошивки в «Мониторе порта» из Arduino IDE, я вручную поймал пару осциллограмм в момент шифрования сообщений. Первые трассы выглядели как-то так.

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


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

Теперь нужно было как-то автоматизировать сбор и сохранение осциллограмм. Rohde & Schwarz на своем сайте предлагает использовать утилиту HMExplorer, но при беглом ознакомлении с документацией я пришел к неутешительному выводу, что ее возможностей мне недостаточно.

Одно дело — просто удаленно управлять прибором с компьютера по интерфейсу GPIB или Ethernet с помощью набора инструкций SCPI. Но мне предстояло еще одновременно забирать энтропию с /dev/urandom и направлять ее в микроконтроллер по последовательному порту. Проще было написать реализацию самостоятельно.

Чуть выше я упомянул набор SCPI — это стандартизированный способ взаимодействия с инструментами самых разных производителей. Краткую справку по командам я нашел в конце документации на сам осциллограф RTC1002, но дополнительно у R&S есть исчерпывающее руководство SCPI Programming Manual.

По сути, во всем большом наборе у нас есть три вида инструкций. Общие команды поддерживают все совместимые по стандарту устройства. Они позволяют узнать, с каким конкретно прибором мы сейчас работаем.

/*  CMD: *IDN?
 *  ANS: ROHDE&SCHWARZ,RTC1002,XXXXXXXXX,YYY.ZZZ
 */

Следующими идут базовые команды, они определяют состояние и работу инструмента. Для них есть полная форма (все символы в сообщении) и сокращенная форма (только символы в верхнем регистре).

/*  CMD: RUNContinous (RUNC)
 *  ANS: None
 */

Наконец, у нас есть команды для конкретных настроек прибора. Они составляют большую часть набора SCPI и для удобства вложены в различные пространства имен. Можно задать частоту дискретизации, вертикальное и горизонтальное отклонение, задержку срабатывания триггера и много чего еще. В общем, тут удаленно доступен буквально любой пункт меню. Более того, можно посылать по сети собственные сообщения и отображать их на экране.

/*  CMD: DISPlay:DIALog::MESSage (DISP:DIAL:MESS)
 *  ANS: None
 *
 *  CMD: CHANnel1:DATA:HEADer? (CHAN1:DATA:HEAD?)
 *  ANS: -9.477E-008,9.477E-008,30000,1
 */

Таким образом, подключив осциллограф к одной сети с ноутбуком и настроив IP-адреса, можно работать с прибором через сокеты из любой программы на С:

#include <stdio.h>
#include <stdlib.h> 
#include <string.h>

#include <sys/socket.h>
#include <arpa/inet.h>
#include <unistd.h>

#define PORT        (5025) 
#define HOST        ("192.168.10.11")
#define SCPI_BUFFER (512)

int scpi_init(char* ip, int port) {
  int sd = socket(AF_INET, SOCK_STREAM, 0);
  struct sockaddr_in server = {.sin_family = AF_INET,
                               .sin_port = htons(port)};

  inet_pton(AF_INET, ip, &server.sin_addr);
  connect(sd, (struct sockaddr *) &server, sizeof(server));

  return sd;
}

int scpi_cmd_write(int sd, char* cmd) {
  return write(sd, cmd, strlen(cmd));
}

int scpi_cmd_read(int sd, char* cmd, char* b) {
  if (scpi_cmd_write(sd, cmd) != strlen(cmd)) {
    return -1;
  }

  return read(sd, b, SCPI_BUFFER);
}


Собираем все вместе

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

int uart_open(char* path) {
  int fd = open(path, O_RDWR | O_ASYNC);
  struct termios attr;

  /* Set parameters for UART communication */
  tcgetattr(fd, &attr);
  cfsetospeed(&attr, B115200);
  cfsetispeed(&attr, B115200);  
  attr.c_cflag &= ~(PARENB | CSTOPB | CSIZE);
  attr.c_cflag |= CS8 | CLOCAL;
  attr.c_lflag = ICANON;
  attr.c_oflag &= ~OPOST;

  tcsetattr(fd, TCSANOW, &attr);
  tcflush(fd, TCIOFLUSH);

  return fd;
}

int uart_recv(int fd, char *b, int len) {
  int i = 0;

  for (char c = 0; i < len && c != '\n'; ++i) {
    read(fd, &c, 1);
    b[i] = c;
  }

  return i;
 }

int uart_send(int fd, char *b, int len) {
  return write(fd, b, len);;
}

int uart_close(int fd) {
  return close(fd);
}

Что касается случайных чисел, то на самом деле их можно было генерировать непосредственно на микроконтроллере. И даже отдельный аппаратный TRNG тут совершенно необязателен. Если стоит задача получить энтропию из разряда «дешево и сердито», можно обойтись и шумами в АЦП, недавно я писал в «Хакер» статью об этом.

char vLUT(char n) {
  return n >= 10 ? n + 'A' - 10 : n + '0';
}

void get_rand(char* buffer, int n) {
  int rd = open("/dev/urandom", O_RDONLY);

  read(rd, buffer, n);
  close(rd);

  for(int i = 0; i < n; ++i) {
    buffer[i] &= 0x0F;
    buffer[i] = vLUT(buffer[i]); 
  }
}

Теперь остается только собрать все это в единый файл main.c, и можно запускать процесс. Хороший момент попить чайку и подумать о том, что делать дальше.

Предобработка

Через некоторое время у меня на ноутбуке уже было пять тысяч осциллограмм в формате CSV и бинарный файл с отладочной платы. Однако перед анализом трасс в статистическом пакете Jlsca данные предстояло конвертировать в формат TRS.

Вообще говоря, предварительно еще следовало убедиться, что общие паттерны на осциллограмме (пики и впадины) соответствуют раундам алгоритма AES и сами трассы синхронизированы. Другими словами, нужно быть уверенным, что мы сравниваем внутреннее состояние микроконтроллера в одинаковые моменты времени, иначе вся остальная работа теряет всякий смысл.

Подобная рассинхронизация вполне может возникнуть из-за фазового дрожания сигнала (джиттер) на канале с триггером, и тут все зависит от характеристик твоего осциллографа, насколько быстро он сможет среагировать и начать захват данных по основному каналу.

К счастью, я мог визуально наблюдать осциллограммы на экране прибора во время сбора трасс питания и через какое-то время пришел к выводу, что этот шаг вполне можно пропустить. Однако в оригинальной презентации инструктору Riscure все же пришлось выравнивать трассы, преимущественно из-за бюджетности модели Hantek.

Вернемся к осциллограммам. Конечно, экспоненциальное представление чисел в столбцах файлов CSV тоже можно было распарсить и сконвертировать средствами языка С. Однако я не смог найти у Riscure описания формата TRS, зато нашелся модуль для Python.

Напишем пару скриптов:

# Remove all timestamps from files
TOTAL = 5000

for i in range(TOTAL):  
  source = open('TRC/TRC{:04}.csv'.format(i), 'r')
  target = open('traces/traces{:04}.txt'.format(i), 'w')

  source.readline() # skip header
  for line in source.readlines():
    time, volt = line.split(',')
    target.write(volt)

  source.close()
  target.close()

Теперь сольем все файлы вместе:

# Create resulting .trs file
import numpy as npy
import trsfile as trs

TOTAL = 5000

target = trs.trs_open('result.trs', 'w', padding_mode = trs.TracePadding.AUTO)

bytes = open('data.bin', 'rb')

for i in range(TOTAL):
  source = npy.genfromtxt('traces/traces{:04}.txt'.format(i), dtype = float)
  target.append(trs.Trace(trs.SampleCoding.FLOAT, source, data = bytes.read(32)))

bytes.close()
target.close()

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

Кстати, если будешь повторять это все в macOS, обрати внимание, что модуль trsfile использует mmap для работы с файлами .trs, а операционка Apple не поддерживает изменение размера уже размещенного в памяти файла. Так что скрипт будет постоянно выдавать ошибку. Самый простой вариант — перейти в Linux и собрать файл там, что я и сделал с помощью Ubuntu в виртуалке.

Кто такая Julia

Анализировать полученные данные предстоит с помощью пакета Jlsca. Это еще одна открытая разработка компании Riscure (ты можешь свободно скачать ее на GitHub). Утилита написана на молодом языке Julia, что в теории обещает хорошую производительность при сложных вычислениях и научных расчетах.

Аргументы при запуске передаются программе несколько необычным образом — через имя файла. Нужно указать алгоритм, режим шифрования и (опционально) правильный ключ. В моем случае название получилось таким: aes128_sb_ciph_47f1ed9166c996b2f553b147be3fbc20.

Кроме того, раз мы вычисляем вес Хэмминга для наших данных, нужно добавить соответствующую строчку в файл main-condavg.jl.

params.analysis.leakages = [HW()]

Скрестить ли пальцы на удачу? Вот еще, какие глупости.

$ julia examples/main-condavg.jl aestraces/aes128_sb_ciph_47f1ed9166c996b2f553b147be3fbc20.trs

Скажу честно, сразу получить правильный результат мне не удалось. Свою первую попытку я испортил тем, что невнимательно прочитал принципиальную схему платы и не удалил все нужные конденсаторы в цепи питания. Мало того, я забыл добавить к трассам наборы «открытый текст — шифртекст». Как итог, программа и близко не смогла угадать ни одного правильного байта в ключе.

Второй подход оказался удачнее, но, видимо, мне не хватило размера выборки. Я собрал тысячу осциллограмм с устройства в момент шифрования, и результаты были гораздо лучше — коэффициенты для нужных байтов попали в топ-10, и это обнадеживало. Однако соотношение шумов и полезного сигнала все еще было далеко от идеала.

Успешной оказалась лишь третья попытка, но, наверное, и это неплохой результат! Пришлось увеличить массив данных в пять раз, а также пересмотреть схему питания, добавив внешний регулятор напряжения на 3,3 В. Не знаю, сыграло это свою роль или нет, но в качестве источника электроэнергии я выбрал не внешний аккумулятор с Li-Pol и импульсным DC/DC-преобразователем, а обычную батарейку на 9 В.


Подводим итоги

Если попытаться описать мои впечатления от проделанной работы одним словом, то, наверное, лучше всего подойдет слово «смешанные». С одной стороны, это было безумно интересно, по ходу дела пришлось решить несколько нетривиальных проблем, разобраться в новых для себя вещах, и в целом о потраченном времени я нисколько не жалею.

Более того, стоит признать, что подобные атаки действительно следует рассматривать в числе угроз для безопасности устройства и конфиденциальности пользовательских данных. Конечно, вытащить ключ шифрования с современного микроконтроллера на ядре ARM оказалось не так просто, как в случае с его предком на AVR, да и оборудование для атаки пришлось использовать более дорогое. Но принципиальных отличий, как видишь, оказалось немного.

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

Дополнительные материалы

Разобрав в деталях существующую угрозу, нельзя не сказать пару слов о способах защиты от нее. Тут я вряд ли придумаю что-то новое, но могу порекомендовать несколько хороших источников, где ты точно найдешь полезную информацию. В первую очередь это сборник рекомендаций компании Riscure для разработчиков и специалистов по безопасности Secure Application Programming. Кроме того, заслуживает изучения и материал «Атаки на встраиваемые системы» компании BI.Zone. АВК по энергопотреблению и АМИС по питанию там разобраны достаточно подробно.

Постскриптум

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

Уязвимы ли они перед такими атаками и насколько это вообще реально с бюджетным оборудованием — подобный момент заслуживает самого внимательного изучения. Во всяком случае, я намереваюсь вернуться к АВК через некоторое время и, возможно, появлюсь с новым материалом. Если, конечно, ты меня не опередишь.

ПОДПИСАТЬСЯ - Бюро121