Жёсткие диски: если вы читаете эту статью, то с большой вероятностью у вас есть одно или несколько таких устройств. Они довольно просты и, по сути, представляют собой набор 512-байтных секторов, пронумерованных возрастающими адресами, также называемыми LBA (Logical Block Address). Компьютер, к которому подключен жёсткий диск (hard drive, HD), может считывать и записывать данные в эти сектора. Обычно используется файловая система, абстрагирующая все эти сектора до файлов и папок.

Неспециалисту может показаться, что оборудование HD должно быть довольно простым: достаточно всего лишь устройства, подключаемого к порту SATA, которое может позиционировать свои головки чтения/записи и считывать или записывать данные на пластины. Однако их работа намного сложнее: разве жёсткие диски не занимаются обработкой сбойных блоки и атрибутов SMART, и не имеют кэша, с которым тоже каким-то образом нужно работать?

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

Элементы печатной платы


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


Разумеется, все мы знаем, как должны работать механические детали жёстких дисков, но меня интересовали не они. Мне важна была небольшая печатная плата, находящаяся на обратной стороне HD, где расположены разъёмы SATA и питания. Вот как выглядят подобные печатные платы:


Видно, что на плате есть примерно четыре чипа. Вот что мне удалось о них узнать:


Это память DRAM. С ней всё проще всего, спецификации находятся легко. Эти чипы могут иметь объём от 8 МБ до 64 МБ, и этот размер связан с размером кэша, который должен иметь жёсткий диск.


Это контроллер двигателя привода шпинделя. Это нестандартная деталь, поэтому спецификации найти сложно, однако у некоторых контроллеров, похоже, существуют братья и сёстры, информация о которых чуть более доступна. Кажется, наиболее распространёнными являются контроллеры ST Smooth; кроме управления двигателем шпинделя они занимаются регулированием мощности и имеют несколько аналоговых/цифровых каналов.


Это флэш-память с последовательным интерфейсом. С ней всё тоже легко, она может иметь размер от 64 КБ до 256 КБ. Похоже, она используется для хранения программы, с которой запускается контроллер жёсткого диска. У некоторых жёстких дисков этого чипа нет, однако вместо него имеется флэш-память внутри чипа контроллера диска.


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


А в этом элементе происходит всё самое интересное: это контроллер жёсткого диска. Они изготовляются Marvell, ST и некоторыми другими производителями больших интегральных схем. Некоторые компании-производители жёстких дисков изготавливают и собственные контроллеры: я замечал, что этим занимаются Samsung и Western Digital. Почти всё остальное не представляет никакой сложности, поэтому меня интересовало это устройство.

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

Итак, у нас нет спецификаций на самые важные интегральные схемы, а значит, мы зашли в тупик… или нет?

Подключаем JTAG


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

На форумах HDDGuru я обнаружил тему пользователя Dejan. Ему каким-то образом удалось повредить внутреннюю флэш-память его жёсткого диска, и он захотел понять, существует ли способ загрузки контроллера с внешней флэш-памяти или способ перезаписи флэш-памяти. Ему не удавалось найти ответа в течение пяти дней, но парень оказался изобретательным: следующим постом он опубликовал сообщение, что он смог найти схему расположения выводов порта JTAG. Это важная находка: порт JTAG можно использовать для управления контроллером как марионеткой. Его можно останавливать, перезапускать, модифицировать память, устанавливать контрольные точки, и т.д. Затем Dejan разобрался, как создать дамп загрузочной ПЗУ контроллера, понял, что на одном из разъёмов жёсткого диска есть последовательный порт, и смог восстановить флэш-ПЗУ своего диска. Затем он выполнил дамп ещё нескольких битов и указателей о процессе обновления флэш, после чего окончательно пропал во мгле Интернета.

Всё это оказалось довольно полезной информацией: по крайней мере, это дало мне понять, что все контроллеры Western Digital, похоже, имеют ядро ARM, к которому можно получить доступ через порт JTAG. Из этой информации я также понял, что у жёстких дисков обычно есть последовательный порт, который чаще всего не используется, но может быть полезен для отладки моего хака. Теперь у меня должно быть достаточно информации, чтобы приступить к взлому.

Моя система выглядит так:


Красная штука — это FT2232H, дешёвая плата, которую можно купить примерно за 30 долларов, на ней есть JTAG и последовательный порт, а также интерфейс SPI. Она подключена к интерфейсу JTAG жёсткого диска, а также как к разъёму, на котором у жёсткого диска есть последовательный порт. Жёсткий диск напрямую подключен к порту SATA на материнской плате моих компьютеров, а также к внешнему блоку питания ATX. Для управления JTAG я использовал ПО OpenOCD.

Теперь вопрос заключается в следующем: будет ли это работать на самом деле? Dejan проделал это с 2,5-дюймовым жёстким диском на 250 ГБ с контроллером 88i6745 и выяснил, что используется ядро arm9. Я взял 3,5-дюймовый диск на 2 ТБ с контроллером 88i9146, он имеет другой форм-фактор и чуть новее. К счастью, OpenOCD имеет возможность самостоятельно распознавать, что находится в цепочке JTAG. Вот что я обнаружил:


Это немного сбило меня с толку… Я ожидал одного tap, соответствующего одному ядру ARM… однако обнаружил три tap. Значит ли это, что у этого чипа три ядра ARM?

Проведя исследование, я выяснил, что это так, похоже, у чипа три ядра. Два Feroceon — довольно мощных ядра наподобие arm9, и ядро Cortex-M3 — ядро послабже, больше напоминающее микроконтроллер. Немного поэкспериментировав (и проведя дальнейшие исследования) я выяснил, что все контроллеры имеют собственные функции:

  • Feroceon 1 обрабатывает физическое чтение и запись с/на пластины жёсткого диска
  • Feroceon 2 управляет интерфейсом SATA
  • Feroceon 2 также работает с кэшем и занимается преобразованием из LBA в CHS
  • Cortex-M3 занимается… ничем? Мне удалось остановить его с сохранением всех функций жёсткого диска.

С какого же ядра начинать взлом? Моя задача заключалась в компрометации защиты системы с помощью модификаций прошивки жёсткого диска. Простейший способ реализации этого, который при этом, вероятно, сложнее всего обнаружить — изменение данных на лету. При этом данные на диске не нужно будет менять, а прошивка может просто сделать себя невидимой. Для этого мне нужно найти подходящее для такого перехвата ядро: мне требуется ядро, имеющее доступ к данным, находящимся в процессе переноса с диска на кабель SATA, которое при этом можно сфальсифицировать для модификации данных, пока они находятся между этими двумя точками.

Как эти данные попадают с пластин HD в интерфейс SATA? Здесь я воспользовался своей интуицией. Рассуждал я примерно так:

Если процессоры используют стандартное копирование памяти при частоте 150 МГц, то они бы могли достичь скорости в 150*23/2=2,4 Гбит/с, а на практике, скорее всего, гораздо меньше. По спецификации жёсткий диск имеет скорость 6 Гбит/с, поэтому, вероятно, используется какое-то аппаратное ускорение. Наиболее вероятным аппаратным ускорением будет применение DMA. Это означает, что данные копируются напрямую из логики чтения головок в память без активного участия процессора. То же самое относится и к порту SATA: процессору нужно просто указывать, где находятся данные, а логика DMA занимается считыванием данных напрямую из памяти.

Если это было так, то где бы располагалась память, на которую бы указывал движок DMA? Неплохим кандидатом является кэш жёсткого диска: считываемые с диска данные всё равно попадают в кэш, поэтому логично будет копировать их туда сразу после чтения с диска. Ранее я выяснил, что за работу с кэшем отвечает Feroceon 2, поэтому он стал основной целью моей попытки взлома.

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


Как видите, привязка к памяти немного фрагментирована. Разбросаны небольшие части ОЗУ, есть пространство IO (ввода-вывода) и IRQ (прерываний), а также немного внутреннего загрузочного ПЗУ. Здесь есть также большой сегмент на 64 МБ того, что, по моему мнению, является чипом DRAM с кэшем на нём. Давайте выясним, так ли это. Сначала я смонтировал диск на свою машину и записал в файл на нём «Hello world!». Смогу ли я найти эту строку в области памяти 64 МБ?


Да, вот и она. Похоже, процессоры Feroceon имеют доступ к кэшу, а сам он привязан к области DRAM 64 МБ.

Инъекция кода


Разумеется, если бы я хотел что-то менять в кэше, то не сканировал бы каждый раз целиком 64 МБ ОЗУ: мне нужно было понять, как работает кэш. Для этого мне нужно было создать дамп прошивки жёсткого диска, дизассемблировать его и понять, чтобы разобраться в функциях кэширования.

Дизассемблирование этой прошивки — нетривиальная задача. Во-первых, в коде перемешаны инструкции ARM и инструкции в стиле thumb. Это раздражает, а у меня нет дизассемблера, способного автоматически переключаться между двумя этими наборами. Кроме того, в коде отсутствует то, что очень упрощает дизассемблирование ПО: обычно процедуры закодированы так, чтобы выводить сообщения типа «Couldn't open logfile!», когда что-то идёт не так. Эти сообщения оказывают огромную помощь, когда разбираешься в том, для чего нужна процедура. Однако в этой прошивке не было таких строк: приходилось разбираться в назначении процедуры только по её коду. Однако кодовая база кажется немного устаревшей, и иногда дизассемблированный код выглядит так, как будто часть функций к нему «прикрутили» позже, что чуть усложнило анализ.

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

После длительного изучения кода и запуска отладчика для проверки моих догадок мне удалось добраться до ядра системы кэширования: таблицы в ОЗУ, которую я назвал «таблицей дескрипторов кэша»:


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

Теперь, когда я раскрыл тайну таблицы дескрипторов кэша, смогу ли я перехватывать запись на диск до того, как она отправится из порта SATA к компьютеру? Чтобы сделать это, мне понадобится возможность исполнения собственного кода в контроллере жёсткого диска. Более того, мне нужно обеспечить запуск кода в нужное время: если я изменю кэш слишком рано, то данные в него ещё не попадут; если я изменю его слишком поздно, то данные уже отправятся на компьютер.

Реализовал я это подключением к уже имеющейся процедуре. Мой хак будет выполняться на Feroceon 2, а этот процессор занимается всей передачей по SATA, поэтому в нём должна быть какая-то процедура, отвечающая за настройку оборудования SATA для получения данных из кэша. Если я найду эту процедуру, то, вероятно, смогу выполнять перед ней собственный код.

После долгих исследований, установки контрольных точек, проб и ошибок я наконец нашёл соответствующую моим требованиям процедуру. Я модифицировал её, чтобы она сначала выполняла мой код. Вот оригинальный код:

000167BE ; r0 - slot in sata_req
000167BE sub_0_167BE:
000167BE                 PUSH    {R4-R7,LR}
000167C0                 MOVS    R7, R0
000167C2                 LSLS    R1, R0, #4
000167C4                 LDR     R0, =sata_req
000167C6                 SUB     SP, SP, #0x14
000167C8                 ADDS    R6, R1, R0
000167CA                 LDRB    R1, [R6,#0xD]
000167CC                 LDR     R2, =stru_0_40028DC
000167CE                 STR     R1, [SP,#0x28+var_1C]
000167D0                 LDRB    R0, [R6,#(off_0_FFE3F108+2 - 0xFFE3F0FC)]
000167D2                 LDRB    R5, [R6,#(off_0_FFE3F108 - 0xFFE3F0FC)]
000167D4                 LSLS    R0, R0, #4

А вот что происходит, когда в код добавляется вызов моего кода:

000167BE ; r0 - slot in sata_req
000167BE sub_0_167BE:
000167BE                 PUSH    {R4-R7,LR}
000167C0                 MOVS    R7, R0
000167C2                 LD      R6, =hookedAddr
000167C4                 BX      R6
000167C6                 .dw     checksumFix
000167C8                 .dd     hookedAddr
000167CC                 LDR     R2, =stru_0_40028DC
000167CE                 STR     R1, [SP,#0x28+var_1C]
000167D0                 LDRB    R0, [R6,#(off_0_FFE3F108+2 - 0xFFE3F0FC)]
000167D2                 LDRB    R5, [R6,#(off_0_FFE3F108 - 0xFFE3F0FC)]
000167D4                 LSLS    R0, R0, #4
...
FFE3F000                 PUSH    {R0-R12, LR}
FFE3F004                 BX      changeThingsInCache
FFE3F008                 POP     {R0-R12, LR}
FFE3F00C                 LSLS    R1, R0, #4
FFE3F010                 LDR     R0, =sata_req
FFE3F014                 SUB     SP, SP, #0x14
FFE3F018                 ADDS    R6, R1, R0
FFE3F01C                 LDRB    R1, [R6,#0xD]
FFE3F020                 BX      0x167CC

Как видите, некоторые исходные инструкции были заменены на переход к новому коду в ранее неиспользовавшейся области ОЗУ по адресу 0xFFE3F000 и дополнительное слово, чтобы гарантировать правильность контрольной суммы области кода. Если бы я этого не сделал, то HD попытался бы загрузить резервную копию со своих пластин, а нам этого не нужно. Код, к которому выполняется переход, исполняет процедуру под названием changeThingsInCache, а затем делает то, что бы сделал заменённый код. Затем он продолжает исполнение в исходной процедуре, как будто ничего не произошло.

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

void hook() {
  foreach (cache_struct in cache_struct_table) {
    if (is_valid(cache_struct)) {
      foreach (sector in cache_struct.sectors) {
        sector[0]=0x12345678;
      }
    }
  }
}

Этот небольшой фрагмент кода при каждом его вызове заменяет первые 4 байта каждого сектора на 0x12345678, поэтому если бы я загрузил это всё на жёсткий диск, то должен был бы увидеть это число в начале каждого считываемого сектора. Я загрузил фрагменты кода по JTAG…


И вот результат:


Сохранность кода


Разумеется, я мог превратить это в завершённый хак, но из-за необходимости подключения JTAG для записи в ОЗУ при каждом запуске жёсткого диска он становится довольно бесполезным. Мне нужно было сделать его сохраняемым, то есть хранить мои модификации в каком-то месте, откуда их можно было бы брать при каждом включении жёсткого диска.

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


Что же нам записать во флэш-память? К счастью, хранящийся в чипе формат уже был разобран: он состоит из множества блоков данных, а в самом начале находится описывающая их таблица. Эта таблица описывает местоположение блока во флэш-памяти, способ его сжатия (если он сжат), место, в которое блок должен помещаться в ОЗУ, а для последнего адреса — точка исполнения, в которую загрузчик должен перейти для исполнения программы.

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

Разумеется, для этого мне нужно было дизассемблировать и снова собрать двоичный файл флэш-памяти. Я создал для этого инструмент и дал ему скучное название «fwtool». Этот инструмент может дампить разные блоки флэш-памяти, а также транслировать заголовок в текстовый файл для простоты редактирования. Затем можно модифицировать, удалить или добавить блок, а затем собрать всё снова в единый файл прошивки, готовый к записи во флэш-память. Я использовал его для добавления в образ собственного фрагмента кода, прошил всё обратно в чип, припаял чип к жёсткому диску, снова всё запустил и в результате получил вот это:


Это неудивительно: именно такой результат я и получал раньше. Хитрость заключается в том, что теперь для этого не требуется модификация через JTAG.

Прошиваем ПО


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

Инструменты обновления прошивок Western Digital доказывают, что это возможно: по сути, это инструмент, запускаемый под DOS для записи новой прошивки во флэш-память и в сервисную область, то есть на зарезервированные сектора жёсткого диска. По информации из Интернета, для этого в инструменте используются так называемые Vendor Specific Commands. Существуют и другие инструменты, способные модифицировать прошивки: например, есть экспериментальный код, способный использовать незанятые зарезервированные сектора для сокрытия данных. Кроме того, есть набор инструментов под названием idle3-tools, которые можно использовать для модификации байта в прошивке, чтобы изменить поведение жёсткого диска в режиме ожидания. В этом коде тоже используются VSC при помощи «официального» способа с применением ioctls scsi-драйвера Linux. Я решил «позаимствовать» этот код, немного его изменить и интегрировать в fwtool. После экспериментов с кодом и подбора параметров VSC моя программа fwtool внезапно научилась считывать и записывать флэш-память жёсткого диска, подключенного к компьютеру, на котором она запущена.

На этом моя атака была завершена. Если бы blackhat-хакер каким-то образом получил бы root-доступ к серверу с этим диском, то он бы смог использовать fwtool для удалённого создания дампа флэш-памяти на диск, модифицировать его и снова прошить в память. Рано или поздно владелец компьютера выяснил бы, что я использую его машину с вредоносными целями, и, вероятно, переустановил бы систему, закрыв лазейку, через которую хакер проник изначально.

Однако после установки прошивки нападающий смог бы приказать жёсткому диску сделать что-нибудь вредоносное с новой установленной системой. Сначала бы ему нужно было запустить подобное поведение, и это можно было бы реализовать определённой магической строкой, которую бы хак прошивки искал на диске. Магическая строка может быть любым файлом; например, нападающий мог бы загрузить на сервер файл .jpeg со строкой в нём. Также он мог бы запросить файл с веб-сервера через магическую строку, присоединённую к URL. Затем бы она появилась в логах машин, что привело бы к срабатыванию эксплойта.

Затем хак прошивки жёсткого диска сделал бы нечто вредоносное. Например, он мог бы подождать, пока машина не начнёт считывать файл /etc/shadow, где в системе Unix/Linux хранятся все пароли, и модифицировать его содержимое на лету, заменив его чем-то, что хакер прописал заранее. Затем нападающий мог бы попробовать войти в систему с собственным паролем, а машина бы сравнила этот пароль с модифицированным /etc/shadow, позволив нападающему снова войти в систему.

Вот демонстрация, которую я сделал для презентации. Вы видите, как я безуспешно пытаюсь войти в аккаунт root компьютера. Затем я включаю хак и передаю ему хэш нового пароля, а именно пароля «test123». Так как Linux кэширует файл shadow (как и все файлы, к которым недавно выполнялся доступ), мне придётся генерировать сильную дисковую активность, чтобы «вытеснить» файл из кэша; благодаря этому, когда я снова попытаюсь выполнить вход, Linux будет вынужден снова получить файл shadow с диска. Наконец, благодаря очистке кэша я смогу просто выполнить вход в аккаунт root с фальшивым паролем test123.


Другие способы применения


Разумеется, восстановление доступа к серверам, у которых были закрыты тайные способы входа — не единственный полезный способ применения моей работы по реверс-инжинирингу. Её можно использовать и в целях защиты.

Например, можно создать неклонируемый жёсткий диск: жёсткий диск будет функционировать обычным образом, если паттерн доступа к секторам является случайным, аналогично тому, как к файловой системе обращается ОС. Если доступ к диску выполняется только последовательно, как это обычно делает утилита клонирования дисков, то жёсткий диск может искажать данные, делая клон отличным от оригинала.

Контроллер диска интересен и просто как плата контроллера. У него есть три довольно мощных процессорных ядра, к которым подключён достаточно большой объём ОЗУ. Также у него есть uart для последовательного порта и не менее двух интерфейсов SPI; один для флэш-ПЗУ, второй — для контроллеров двигателя шпинделя. Можно загружать код для процессора, обновляя данные во внешнем флэш-чипе или даже при помощи последовательного в загрузчике. Чтобы продемонстрировать мощь чипа, я портировал на свой жёсткий диск довольно популярное ПО. Демо является только проверкой концепции, единственное работающее периферийное устройство — это последовательный порт, а пользовательского пространства пока нет. Тем не менее, я всё равно немного горд тем, что установил на свой жёсткий диск Linux. Сверху расположена стандартная командная строка (жёсткий диск смонтирован в /mnt), снизу — вывод моей работы с последовательным портом жёсткого диска:


Расскажу немного подробнее о том, что здесь происходит: ядро и init каждый упакованы во фрагменты размером ровно в один сектор, а к их началу добавлены магическая строка и порядковый номер. При считывании файла с диска он оказывается в кэше диска. Запись магической строки «HD, lnx!» заставляет модифицированную прошивку искать в кэше все сектора, пересобрать образ ядра и загрузить его. Ядро собрано для процессора без MMU (у контроллера диска его нет) и оно имеет только драйвер для последовательного порта. К сожалению, для ядра без MMU требуется ещё и специально отформатированное пользовательское пространство. Мне не удалось его скомпилировать, поэтому ядро в конце концов выдаёт kernel panic, потому что не может найти init, который оно смогло бы исполнить.