Приключения с Xbox 360: долгий путь к RGH3
- пятница, 24 января 2025 г. в 00:00:16
Всем привет из Positive Labs! Мы исследуем и разрабатываем различные интересные железки. Как обычно бывает, NDA накладывает ограничения на самое вкусное. И все же очень хочется познакомить вас с тем самым исследовательским духом. К счастью, рабочий процесс очень похож на то, чем я занимался в свободное время (и что вечно откладывал из-за нехватки оного), а значит, пришло время продолжить цикл статей по Xbox 360! Да-да, сегодня речь пойдет о том, как появился самый популярный метод модификации «X-коробки» в наши дни — RGH3.
Несколько лет назад я подумал, что эра Xbox 360 прошла, исследования забиыты, и потихоньку начал избавляться от наследия студенческих времен: приставки продал, запчасти раздал, на форумы залезать перестал. Но у вселенной были совершенно другие планы, стоившие мне нескольких месяцев бессонных ночей. Началось все с известной онлайн-барахолки. Друг попросил подобрать что-нибудь для игр: ПК слабоват, денег нет, а поиграть хочется. Первая мысля — самое время для Xbox 360. Совсем недорого и тянет даже GTA V!
Приставку мы купили, прошили, дело сделано. Все? Как бы не так. Во-первых, накатила ностальгия по прошивкам. Во-вторых, барахолка продолжила мне подсовывать рекомендовать объявления с приставками и запчастями по интересной цене, как будто дразня.
А потом произошло вот это.
Жмых Парень за копейки продавал две приставки после неудачной попытки прошивки. Все можно было починить в пару движений паяльником, что я и предложил. Показал, как чинить, рассказал в целом про приставки и, сам того не заметив, вернулся на сцену.
Дальше все пошло вообще не по плану: в дискорд-чате по Xbox 360 кто-то выложил купленные на eBay схемы плат для всех ревизий консоли. На этих схемах Josh Davidson (Octal450) неожиданно для всех обнаружил вывод CPU_PLL_BYPASS в Slim-версиях Xbox 360:
Несмотря на то что контактов для этой сигнальной линии на плате нет, разработчики оставили переходное отверстие, к которому после зашкуривания можно припаяться:
Всю важность этой находки можно понять, погрузившись глубже в историю разработки Reset Glitch Hack (RGH). Детально с особенностями RGH можно ознакомиться в третьей части этого цикла статей, а я же кратко введу вас в курс дела.
RGH, как и другие глитч-атаки, использует факт некорректной работы процессора в определенных условиях. Это может быть слишком высокое или слишком низкое напряжение питания, помехи на опорной частоте процессора или, как в нашем случае, очень короткий импульс низкого уровня на линии сброса (CPU_RESET) процессора.
При обычной установке низкого уровня на этой линии процессор должен перезагрузиться, сбросить свое состояние и начать выполнять код с самого начала, с ROM. Но из-за очень короткой длительности сигнала этого не происходит: процессор продолжает выполнять текущую программу, немного глюкнув при этом, например пропустив выполнение команды или «забыв» записать регистр.
Для такого контролируемого глюка мало подать очень короткий импульс, нужно еще и подать его в нужный момент времени. Чтобы повысить точность попадания и шансы на успех, процессор замедляют. В первом варианте RGH для ревизий Fat замедление происходило через подачу высокого уровня на ту самую линию CPU_PLL_BYPASS, после чего процессор начинал работать в 128 раз медленнее. В этом замедленном режиме тем самым импульсом можно было обойти проверку цифровой подписи:
До недавнего времени считалось, что у Slim-версий приставок нет CPU_PLL_BYPASS, и замедление приходилось выполнять через уменьшение опорной частоты CPU конфигурацией по шине I2C. Но таким образом получалось замедлить процессор всего лишь в 3–4 раза (метод RGH2). К сожалению, после очередного обновления системы Microsoft перевели Fat-приставки на «двойной» режим загрузки, как у Slim-версий, после чего для них тоже пришлось использовать этот метод, так как в режиме 128-кратного замедления время ожидания нужного POST-кода стало непозволительно велико.
RGH2 остался единственным методом для Fat-приставок на долгие шесть лет. Работал он нестабильно, запуска приставки можно было ждать несколько минут (а то и не дождаться вовсе). Были попытки увеличить точность отправки глитч-импульса — чипы CR3 Pro (и его клон x360ace) использовали внешний генератор 150 МГц (против 48 МГц в первых решениях), и даже это не всегда спасало:
Но не подумайте, что сцена пустовала, — были и интересные проекты. В чипах R-JTAG и CR4 XL от тех же Team Xecuter был реализован другой, революционный подход — глитч проверки фьюзов для запуска старого загрузчика, из которого уже работал проверенный SMC JTAG Hack (про который можно почитать в первой части серии). И если R-JTAG использовал замедление по опорной частоте, то в CR4 XL применили тот самый CPU_PLL_BYPASS, что снова вернуло стабильность Fat-приставкам. Но чипы были дорогие, в ограниченном количестве, и широкого распространения не получили.
В конце концов примерно одновременно произошло два события: у меня появилось свободное время (дипломная пора), а DrSchottky решил сделать свой аналог для CR4 XL. За месяц-другой вечерних посиделок были разработаны методы RGH 1.2 и R-JTOP (конкуренция мотивирует). R-JTOP повторял идею CR4 XL — запуск JTAG Hack с помощью глитча проверки фьюзов и CPU_PLL_BYPASS. А вот RGH 1.2 объединил плюсы RGH1 и RGH2, запуская «двойную» загрузку одновременно с использованием CPU_PLL_BYPASS, снова давая идеальную стабильность и в то же время совместимость с RGH2-софтом.
Как это было реализовано? Основной вопрос, заинтересовавший меня тогда, — почему CPU_PLL_BYPASS перестали использовать? Информации про это нигде не было, может, его заблокировали на аппаратном уровне? Быстрая проверка на логическом анализаторе показала, что нет, все еще замедляет в 128 раз. Почему бы тогда не использовать его? Да, на таком замедлении ожидание этапа подсчета контрольной суммы займет очень много времени. Но зачем включать замедление так рано? Нам ведь главное «покрыть» замедлением только момент проверки цифровой подписи (и начало POST-кода перед ним для референса). Поэтому можно немного отложить включение CPU_PLL_BYPASS, чтобы оно произошло как раз перед проверкой, и получить следующий алгоритм глитча приставки:
ожидание POST-кода 0xD9 (или 0xD8, если смотреть по линии POST_1);
ожидание некоторого числа миллисекунд;
включение PLL_BYPASS;
ожидание POST-кода 0xDA;
ожидание другого некоторого числа миллисекунд;
глитч-импульс на RST.
Правильный подбор значений длился несколько дней, но все получилось.
Проект я с тех пор подзабросил, а исходники передал тому самом Джошу aka Octal450, который продолжил работу и сделал:
обновленную версию RGH 1.2 для других модчипов;
версию с CPU_EXT_CLK (замедляет примерно в 10,7 раза, а не в 128) для Fat-ревизий Xenon и Zephyr, где использование CPU_PLL_BYPASS приводит к зависанию;
расширенный набор функций для утилиты J-Runner (до сих пор поддерживает и добавляет фичи).
И вот теперь в его руки попадает тот самый даташит с расположением CPU_PLL_BYPASS для Slim-приставок. Теперь понимаете грандиозность момента? Конечно же, он сразу пошел портировать наработки и подбирать параметры замедления (глитча) уже для Slim-версии. В отличие от Fat-ревизий, использование CPU_PLL_BYPASS на Slim давало замедление не в 128 раз, а в целых 640. Из-за этого подбор параметров затянулся.
Здесь уже и я подключился к исследованию. Чтобы ускорить подбор параметров, я собрал особый загрузчик, который повторял код в бесконечном цикле, и уже на это непотребство натравил чип. С таким стендом параметры довольно быстро удалось определить, после чего я передал их Джошу для доработки.
И тут у меня возникла безумная идея. Если есть настолько крутое замедление, можно ли обойтись совсем без чипа с его высокой точностью? В приставке есть System Management Controller (SMC) — независимый микроконтроллер на ядре Intel 8051, работающий на частоте 48 МГц. На его основе делали JTAG SMC Hack, да еще и линия CPU_RESET на него идет...
Где-то на этом моменте я накупил различных материнок от Xbox 360, обложился логическими анализаторами и с головой ушел в очередное исследование внутренностей приставки…
Как и во многих других системах, помимо основного CPU, в Xbox 360 имеется дополнительный чип SMC, который выполняет вспомогательные задачи:
реакция на кнопки (питания, извлечения диска);
подача питания на компоненты приставки;
согласование вывода изображения (AV или HDMI);
контроль температуры и кулеров.
В ноутбуках такая штука зовется Embedded Controller (EC), но идеи схожи. SMC всегда включен, потребляет немного тока и у него есть своя прошивка!
Конечно, прошивка эта зашифрована и имеет контроль целостности, но алгоритм давно известен, а никакой проверки цифровой подписи сюда не завезли:
def decrypt_smc(data):
key = [0x42, 0x75, 0x4e, 0x79]
res = bytearray()
for i in range(len(data)):
j = data[i]
mod = j * 0xFB
res.append(j ^ (key[i & 3] & 0xFF))
key[(i + 1) & 3] += mod
key[(i + 2) & 3] += mod >> 8
return bytes(res)
Интересно, что алгоритм есть и в коде гипервизора Xbox 360.
Читать или писать прошивку тоже не проблема — интерфейс SPI + два дополнительных контакта.
Один из контактов — Reset-линия SMC, а другой — DBG_EN.
При перезагрузке с удержанием DBG-линии SMC входит в специальный режим, когда по SPI можно читать и писать NAND приставки. Кстати, это нашли благодаря изучению Sidecar от девелоперской версии Xbox 360 (XDK).
Как раз к ней подключены те самые разъемы на плате, куда паяют программатор. Такая архитектура помогает разработчику восстановить приставку, даже если NAND целиком стерт.
К этому еще вернемся в самом конце, а пока что — дизасм и раскрытие секретов!
Если сходу загрузить в дизассемблер расшифрованный SMC, то в самом начале будет некая непонятная белиберда, не похожая на адекватный программный код.
На самом деле, для дополнительной защиты от анализа первые 4 байта SMC — случайные, чтобы код в каждой приставке был зашифрован уникальным образом. «Настоящие» 4 начальных байта находятся в самом конце прошивки.
Если их поместить в начало, получим гораздо более осмысленный код с прыжками на различные обработчики прерываний.
Одна тайна разгадана, осталось много. Как встроиться в прошивку? Как управлять периферией? Как вообще написать что-то под этот чип?!
Часть информации можно почерпнуть из двух других проектов — SMC JTAG Hack и CR4 XL (пропатченные прошивки для SMC с их кодом можно найти в комплекте с утилитой J-Runner). Известно, что код в первом проекте использует GPIO для конфигурации GPU по JTAG:
А код CR4 XL, в свою очередь, следит за состоянием GPIO и отправляет по I2C соответствующие команды.
Если сопоставить номера битов в аппаратных регистрах со схемой приставки, то можно заметить, что они соответствуют портам GPIO, к которым подключены соответствующие линии.
Копаясь тем же образом в коде дальше, можно найти все регистры, отвечающие за GPIO:
SFR_80,90,A0,C0,C8 = GPIO_P0..P4 — значения на выводах GPIO
SFR_A2..A6 = GPIO_P0..P4_DIR — режим пина (input или output)
SFR_9D..9F,A1,A7 = GPIO_P0..P4_OD — настройка open-drain-режима
Теперь можно шевелить пинами и выводить что-то осмысленное на логический анализатор! Но для этого сначала нужно скомпилировать код. Поскольку придется встраиваться в существующую прошивку, писать нужно прямо на ассемблере. К примеру, ниже — код простейшей функции, которая на короткое время взведет один из неиспользуемых пинов SMC в единицу. Функция будет размещена по смещению 0x31E0 — это первый неиспользуемый адрес в оригинальной прошивке SMC для ревизии Corona.
DBG_PIN equ 080h.5
org 031E0h
setb DBG_PIN
clr DBG_PIN
ret
end
Минималистично? А то! После сборки получилось всего 5 байт.
Теперь его нужно каким-то образом засунуть в оригинальную прошивку. Удобнее всего это сделать, если встроиться в основной цикл исполнения. В микроконтроллерах вся логика обычно реализуется через finite-state machine (FSM, конечные автоматы). И все эти автоматы по очереди выполняются в главном цикле.
Малый цикл содержит задачи, требующие немедленной реакции (например, обработка I2C). В большом же расположены задачи, которые требуется запускать с определенным интервалом, так они отсчитывают время до тайм-аута (например, обработка кнопок, переключение состояний питания, моргание светодиодом).
Думаю, ничего страшного не случится, если заменить задачу моргания диода (которого на магазинной плате, кстати, не бывает) на вызов нашей процедуры:
import struct
def patch(data, offset, new):
return data[:offset] + new + data[offset + len(new):]
def lcall(offset):
return b"\x12" + struct.pack(">H", offset)
CODE_START = 0x31E0
smc = open("smc_corona.bin", "rb").read()
code = open("build.bin", "rb").read()[CODE_START:]
smc = patch(smc, CODE_START, code)
smc = patch(smc, 0x829, lcall(CODE_START))
smc = patch(smc, 0x256B, b'\x90') # P0.5 OUT dir
open("new_smc.bin", "wb").write(smc)
В результате на паде DB3R4, который соответствует порту 80h.5, можно видеть сигнал с периодом 20 мс.
Теперь хоть какой-то, но дебаг имеется. Неплохо было бы иметь дебаг получше, например, UART. И — бинго — на схеме платы он есть:
Обычно UART соответствует некоторый аппаратный регистр, запись в который приводит к посылке данных. И чтобы найти этот самый нужный регистр, я, конечно же, писал во все регистры подряд и смотрел, что происходит на аппаратной линии (так, конечно, лучше не делать: за время брутфорса я успел повключать регуляторы питания, наткнуться на регистр watchdog и пару других вещей). И таки нашел. Запись данных в регистр E7 приводит к импульсам на линии.
Похоже на UART, но нет, картина всегда одинаковая, что бы я в регистр ни писал.
Дальнейшие эксперименты с брутфорсом соседних регистров принесли результаты. Оказывается, нужно дополнительно включить UART (SFR_E8 = 0xC0) и настроить его скорость (SFR_E9 = 0xFF, максимальная скорость — 1 500 000 бод). После этого уже получаем нормальный вывод UART.
В регистрах есть биты состояния, по которым можно определить, завершился ли вывод текущего символа и можно ли печатать следующий. Но для простоты реализации я подобрал значение задержки, за которое символ гарантированно успевает выводиться для текущей конфигурации скорости:
put_uart:
mov 0E7h, A
mov A, #09h
loop_uart:
djnz 0E0h, loop_uart
ret
Благодаря этому в UART уже можно писать более осмысленные вещи:
Одного лишь CPU_PLL_BYPASS для стабильного глитч-хака из SMC, по моим предположениям, было недостаточно. Даже с замедлением в 640 раз длина импульса, который порождается кодом ниже, была слишком велика (по сравнению с импульсом в RGH1):
clr 080h.6
setb 080h.6
Значит, помимо активации CPU_PLL_BYPASS, нужно дополнительно замедлять процессор по I2C, как это делали в RGH2, снизив опорную частоту 100 МГц, которую выдает HANA.
Увы, в открытом доступе нет информации, как именно в RGH2 происходит замедление. С помощью логического анализатора удалось достать только несколько значений, которые использовались в модчипах и проекте CR4 XL:
[CF] = 0x40’44’AC’C0 — Corona, 100 МГц, обычный режим;
[CF] = 0x08’44’40’40 — Corona, 33,3 МГц, замедленный режим (CR3 Pro / CR4);
[CF] = 0x08’54’54’30 — Corona, 25 МГц, замедленный режим (RGH2);
[CD] = 0x02’0C’80’4E — Falcon, Jasper, Trinity, 100 МГц, обычный режим;
[CD] = 0x03’80’08’4E — Falcon, Jasper, Trinity, 31,5 МГц, замедленный режим.
Почему значения именно такие? Какие другие частоты можно задать? В обсуждениях упоминалась информация, что на совсем низких частотах процессор работает нестабильно, — правда ли это? Давайте узнаем!
Для этого нужно проверить частоту опорного сигнала, поступающего от HANA к процессору. Логично было бы сделать это с помощью осциллографа, и таковой у меня имелся — DSCope U2P20.
Одна загвоздка: на процессор поступает 100 МГц, сигнал дифференциальный и низковольтажный, а осциллограф работает только с частотами до 50 МГц, и то — если повезет. Чтобы производить замер частоты, нужно либо более крутое оборудование (это сейчас в Positive Labs у меня появилась огромная лаборатория под боком, а тогда приходилось довольствоваться имеющимся), либо каким-то образом поделить частоту, допустим, раз в 10, а затем измерить осциллографом, что я и сделал.
Чем же можно поделить частоту? Под рукой было несколько модчипов для Xbox 360 на базе CPLD Xilinx CoolRunner II, как на рисунке ниже.
Обратите внимание, что на фотографии на чипе нет маркировки. И нет, она не затерта. Изначально эти модчипы делались на именно таких 44-ногих чипах, XC2C64A-VQ44. Народ привык, покупал именно эти, но вот незадача: они закончились. Зато остались те же самые XC2C64A в других корпусах.
Увы, люди берут то, к чему привыкли, и, несмотря на почти полную идентичность (у них был другой ID и не весь софт их поддерживал), популярностью эти модчипы не пользовались. И китайцы придумали решение … Они подумали: «我們只需要在板子前面裝上塑膠誘餌,然後把真正的晶片藏在背面就可以了 Давайте спереди на плате будет просто пластиковая обманка, а настоящий чип спрячем сзади!»
Вот на таком модчипе я и соорудил «делитель частоты» (болванку отпаял, чтоб не мешалась).
Несмотря на то что в этом CPLD нет поддержки дифференциальных линий, благодаря тому, что в Xbox 360 диффпары HANA формируются двумя линиями push-pull, работающими в противофазе, получилось корректно снимать уровни относительно GND. Правда, пришлось понизить напряжение I/O Bank до 1,3 В. Вот такой VHDL-проект с уровнями LVCMOS12 отлично делит частоту на 10:
process(PIN_B) is
begin
if PIN_B'event then
divcnt <= divcnt + 1;
if divcnt = 9 then
temp <= not temp;
divcnt <= 0;
end if;
PIN_D <= temp;
end if;
end process;
Ну, почти отлично: при 100 МГц на выходе получается неравномерная колбасня, оцениваемая осциллографом на 9,31 МГц, но на чуть меньших частотах все в порядке.
На большей части ревизий Xbox 360 (Zephyr, Falcon, Jasper, Trinity) механизм опорной частоты работает одинаково, поэтому разумно начать анализ именно с них. В RGH2 для замедления нужно сконфигурировать регистр 0xCD. Для этого я начал с некоторого значения, а затем менял каждый из битов регистра и фиксировал получаемую с помощью осциллографа частоту. Для работы с I2C я воспользовался открытым проектом pyFTDI.
from pyftdi.i2c import I2cController
import struct
i2c = I2cController()
i2c.configure('ftdi://ftdi:2232h/1')
slave = i2c.get_port(0x70)
def read_cd():
(_, result) = struct.unpack("<BI", slave.exchange([0xCD], 5))
return result
def write_cd(val):
slave.write_to(0xCD, struct.pack("<BI", 4, val))
Тщательно замерив результаты для комбинаций битов, я получил вот такую табличку:
# xxxxxx 010 000110 00000100001001110 - 12.0 MHz |
Итого получается:
первые 6 бит не влияют на частоту;
следующие 3 бита — делитель № 1 (минус единица);
следующие 6 бит — делитель № 2 (минус единица);
следующие 9 бит — множитель (минус единица);
последние 8 бит — коррекция.
Например, если взять конфигурацию по умолчанию (0x20C804E), то можно получить:
0x20C804E = 100 000110 010000000 01001110
D1 = 4 + 1
D2 = 6 + 1
B = 128 + 1
O = 78
Тогда итоговая частота равна N × 129 / 5 / 7 + 78 × D, где
N — частота системного кварца (27 МГц)
D — множитель коррекции (~0,00214 МГц)
Занимательный факт: по формуле выходит, что опорная частота Xbox 360 этих ревизий равна около 99,68 МГц.
В ходе экспериментов с различными частотами выяснилось, что менять коэффициенты PLL «на ходу» затея не очень хорошая: при очень низких значениях частота не меняется либо меняется неправильно, при очень высоких — частота перестает генерироваться совсем. Но самое нехорошее — что при одних и тех же параметрах и самых идеальных условиях частота может совсем незначительно, но отличаться!
Напомню, что для высокой точности глитч-хака обязательно нужно попадать в один и тот же момент. А здесь он «плавает». Теперь становится понятно, почему результаты RGH2 были такие рандомные. Чтобы исправить этот недочет, я начал искать, каким же регистром можно перезагрузить PLL: обычно в оборудовании смена конфигурации требует выключения и последующего включения аппаратного модуля — вдруг поможет.
Для этого я начал менять биты соседних регистров и одновременно смотреть на вывод осциллографа. И такой брутфорс принес результаты! Правда, нашел совсем не то, что искал, но вышло даже лучше. Старшие 8 бит регистра 0xCE управляют источником опорной частоты CPU:
0x08E84014 — стандартный режим, PLL;
0x28E84014 — Bypass EXT (27 МГц) — частота напрямую с внешнего кварца;
0x48E84014 — смена режима регистра 0xCD, делители и множители работают как-то странно (не изучал, возможно, это PLL_INT);
0x88E84014 — Bypass INT (предположительно, от 3 до 10 МГц) — вероятно, частота напрямую с внутреннего генератора, сильно плавает;
0xC8E84014 — None (нет генерации).
И режим Bypass EXT отлично подходит для глитча: частота всегда стабильна, включается и выключается одной командой без каких-либо последствий, в отличие от перенастройки PLL.
В ревизии Corona чип HANA отсутствует — его функциональность теперь находится в южном мосте. Регистры генератора частот тоже поменяли свое назначение.
С помощью аналогичного перебора и экспериментов с регистром 0xCF была получена следующая табличка:
#0x100 - 132 MHz |
Из этого выходит, что последние 3 бита отвечают за делитель, но несколько необычным образом:
00 — x1
01 — x2
10 — x4
11 — x3
А следующие за ними 8 бит — множитель. И формула для конечной частоты имеет вид M / D × 100/48, где
M — множитель,
D — соответствующий делитель.
Наименьшая стабильная частота, которую удалось выставить,— 3,6 МГц, что очень даже неплохо. Увы, ускоряется и замедляется вообще все: GPU, CPU и даже SMC (это легко заметить по изменившейся частоте работы UART). А раз SMC замедляется, замедлится и код, который выполняется внутри и реализует глитч-хак, поэтому замедление через HANA PLL, увы, не годится.
При таком замедлении вращающийся кулер начинает издавать специфический звук, причем тон звука зависит от заданной частоты. Device Orchestra, ловите идею!
К счастью, как и на предыдущих ревизиях, брутфорсом регистров HANA был обнаружен режим Bypass и для ревизии Corona. Для этого достаточно в регистре 0xDB установить 3 бит:
0x01F001F8 — Bypass EXT (25 МГц),
0x01F001F0 — стандартный режим, PLL.
После этого на CPU вместо частоты 100 МГц начинает поступать 25 МГц напрямую с системного кварца, чего вполне достаточно для замедления.
В процессе перебора также был обнаружен аналогичный Bypass для GPU (регистр 0xDB, 0x01F801F0) и для pixel_clk (регистр 0xDC, 0x000001F8).
Как я писал выше, на старых Fat-ревизиях отключение PLL замедляет процессор всего в 128 раз — в 5 раз меньше, чем на Slim-версии. К сожалению, такая величина замедления оказалась все еще недостаточной и глитч зачастую не срабатывает из-за тормознутости SMC. То в нужный момент не попадает, то импульс недостаточно короткий. Вариантов решения тут два: либо использовать конфигурируемое замедление через I2C (чревато нестабильностью из-за особенностей HANA PLL), либо… разогнать южный мост!
Изначально он работает на частоте 48 МГц, но ведь эта опорная частота тоже генерируется HANA и может быть изменена! В итоге снова был выполнен брутфорс регистров и было определено, что за частоту южного моста отвечает регистр 0xD4. Требуемый уровень разгона был с легкостью достигнут через изменение младших битов:
0x990e00e — изначальное значение (48 МГц);
0x990e001e — двукратный разгон (96 МГц);
0x990e026 — максимальное значение, которое смог осилить мой HANA (120 МГц).
Определить полный расклад регистра по битам (делитель, Bypass, множитель) можете самостоятельно на досуге 🙂
С замедлением разобрались, теперь нужно бы определиться, в какой момент работы приставки нужно послать тот самый глитч-импульс.
Процесс загрузки Xbox 360 (на последних версиях системы) происходит следующим образом:
1BL — инициализация FSB, загрузка, расшифровка и проверка CB через RSA-2048;
CB_A — загрузка, расшифровка и проверка CB_B через HMAC-SHA1;
CB_B — инициализация DRAM, загрузка, расшифровка и проверка CD через HMAC-SHA1;
CD — инициализация GPU и прочего «железа», дальнейшая загрузка системы.
И эту цепочку доверия хотелось бы оборвать где-нибудь ближе к началу. В методе RGH2 глитчем атакуют процедуру сравнения хеш-суммы в загрузчике CB_A после POST-кода 0xDA.
Точнее, все думали, что глитчили именно это место. Тщательные эксперименты показали, что на cmpwi и beq reset-глитч не влияет. Зато он влияет на команду addi (mr):
После глитч-импульса вместо реального значения из регистра-источника используется значение 0, что в итоге принимается за успех сравнения. Знание этого факта позволяет найти еще одно интересное место для глитч-атаки, чуть раньше, возле пост-кода 0xD5. И даже не одно, а целых четыре.
Здесь при вмешательстве в любую из выделенных команд получится переполнение при чтении CB_B и перезапись текущего выполняемого кода данными из NAND. Это место удобно тем, что находится недалеко от чтения NAND и в теории позволяет обойтись даже без сигнала POST_OUT в качестве референсной точки. Эта идея была реализована в самой первой пробной версии RGH3, однако реализация не показала достаточной стабильности по сравнению со стандартной и была заброшена до лучших времен.
balika011 заинтересовался этим методом, порасспрашивал детали и сделал свою реализацию «беспостового глитч-хака» (к сожалению, без указания первоисточника).
Итоговый алгоритм атаки через глитч сравнения хеша выглядит приблизительно так:
ожидание POST 0xD6;
включение замедления по HANA;
ожидание POST 0xD8;
задержка перед замедлением X ms;
включение замедления PLL;
ожидание POST 0xDA;
задержка перед импульсом Y ms;
глитч-импульс;
выключение замедления PLL;
выключение замедления HANA;
проверка успешности атаки.
Почему именно такие значения POST-кодов, а не D8, D9, DA? Изначально в RGH1 POST-коды шли с пропусками (0x37, 0x39), и определить их можно было только по первому биту. В RGH2 это упрощало реализацию на не слишком ресурсоемком CPLD (счетчик меньшей размерности). Теперь же, из-за того, что на новых материнках ревизии Corona нет разведенной POST-шины, приходится подсовывать подпружиненный контакт под процессор, и этим способом можно достать POST_OUT_1, но не POST_OUT_0 (0 и 1 — номера битов значения POST-кода):
В отличие от FPGA, на которых изначально реализовывали RGH, микроконтроллер SMC плохо подходит для высокоточных замеров времени. Процесс выполнения может быть потревожен прерываниями, а настройка системного таймера проблематична — нет документации. Самый простой вариант — отключить все прерывания и зависнуть в while(--count), контролируя время ожидания начальным значением обратного счетчика.
Но если долго находиться в цикле, сработает сторожевая псина watchdog и перезагрузит SMC. Можно, конечно, периодически пинать псину обнулять его, но это уменьшает точность и все равно увеличивает время, затрачиваемое на одну попытку запуска. Поэтому чем меньше время ожидания, тем лучше. А что делается между POST-кодами 0xD8 и 0xDA? Расшифровка и подсчет хеш-суммы CB_B. И чем меньше размер CB_B, тем быстрее закончится этот процесс. Дело за малым: нужно уменьшить CB_B до минимального возможного размера!
Из кода CB_A видно критерии корректности CB_B:
размер не более 0xC000;
entry point не менее 0x3D0;
размер кратен 4;
entry point не выходит за пределы размера.
Если дополнительно учесть, что SHA-1 считается блоками по 0x40 байт, то получится, что размер CB_B делать менее 0x400 нецелесообразно: последний блок вычислений все равно будет 0x40 байт. Несмотря на требование о размещении entry point не ранее адреса 0x3D0, остальной код может быть размещен в произвольной точке. Поэтому, если не удается уложиться в 0x400 − 0x3D0 = 0x30 байт, никто не запрещает использовать и меньшие адреса.
Итого получается следующая цепочка загрузки:
1BL загружает и расшифровывает CB_A;
CB_A загружает и расшифровывает CB_X (новый промежуточный загрузчик вместо CB_B);
CB_X загружает уже расшифрованный CB_B;
CB_B загружает уже расшифрованный CD.
Как можно видеть, CB_X добавлен в цепочку только ради уменьшения времени внутри CB_A. Теперь нужно сделать этот самый CB_X, который выполнит единственную задачу — загрузит следующий бутлоадер в цепочке, CB_B.
Чтобы создать свой минимальный, но вполне рабочий бутлоадер, нужно иметь представление о соглашениях вызова этих самых бутлоадеров. Из реверса 2BL можно увидеть, что он:
при старте копирует себя по адресу 0xC000;
берет из r31 данные, где расположено то, что нужно загрузить;
копирует BL из NAND в начало SRAM;
расшифровывает и проверяет данные;
перед следующим переходом устанавливает указатель на следующий BL в r31.
Таким образом возникает некая эстафета. Каждый загрузчик проверяет своего соседа и одновременно передает в r31 указатель уже на соседа соседа.
Поскольку заморачиваться с шифрованием и проверкой не требуется, при создании бутлоадера нужно обеспечить:
копирование создаваемого бутлоадера в 0xC000;
загрузку CB_B;
увеличение указателя в r31 на размер кода (CB_X);
передачу управления CB_B.
Первая часть реализуется довольно просто — достаточно знать нужные адреса:
sub_3D0: # lowest possible entry point
li r3, 0x200
oris r3, r3, 0x8000
sldi r3, r3, 32
oris r4, r3, 1 # 0x8000020000010000, SRAM address
addi r5, r4, -4 # will be used in the second part
ori r6, r4, 0xC000 # 0x800002000001C000, where to copy
li r2, 0x7F # 0x80 * 8 bytes
mtctr r2
copy_second_stage: # memcpy cycle
ldu r2, 8(r4)
stdu r2, 8(r6)
bdnz copy_second_stage
b 0xC350 # jump to the second part
После этого нужно передать информацию SMC о том, что загрузчик успешно запустился через выставление POST-кода. Делать это нужно с небольшой паузой, чтобы SMC успел это увидеть. Предыдущий POST-код 0xDB имеет единицу в первом бите, соответственно, чтобы SMC увидел изменение, нужно установить нолик. Значение 0x54 отлично подойдет:
oris r6, r3, 6 # 0x8000020000060000, I/O
li r2, 0x54 # bit ‘1’ must be 0 here
sldi r2, r2, 56
std r2, 0x1010(r6) # POST 0x54 to tell SMC we're done
lis r2, 1 # small delay for the POST
mtctr r2
sleep_cycle:
nop
bdnz sleep_cycle
В конце нужно выполнить копирование и запуск CB_B. Код можно спереть подсмотреть в дизасме официальных бутлоадеров:
oris r6, r3, 0xC800 # 0x8000020000C80000, mapped NAND
addi r6, r6, -4
clrldi r2, r31, 32
add r6, r6, r2 # + provided r31 offset
lwz r4, 0x10(r6) # CB_B size
lwz r3, 0xC(r6) # CB_B entry point
add r31, r31, r4 # update r31 offset
srdi r4, r4, 2
mtctr r4
copy_cbb: # copy CB_B to the SRAM
lwzu r2, 4(r6)
stwu r2, 4(r5)
bdnz copy_cbb
clrlwi r3, r3, 16 # prepare jump address
addis r3, r3, 0x200
mtlr r3
blr # jump to the CB_B
Итого полученный код занял лишь 144 байта из примерно 1000 доступных! Неплохо.
Чтобы все правильно загрузилось, нужно корректно состыковать все кусочки пазла, необходимые для запуска приставки: SMC, CB_A, CB_X, CB_B (под нужную материнку, пропатченный), CD (кстати, опенсорсный) и XeLL (Xenon Linux Loader). В качестве базового источника информации и функций я использовал существующие сборщики подобных образов для Xbox 360 (например, build.py для RGH1 и RGH2, исходники Xebuild GUI, JRunner).
Начнем с заголовка. Он расположен в самом начале NAND и в нем указана информация, откуда нужно загружать код SMC для южного моста и 2BL для CPU. Ну и копирайт производителя приставки.
Как шифруется SMC, я рассказывал в начале статьи. Его нужно разместить по указанному адресу — тут ничего сложного. CB_A зашифрован алгоритмом RC4 с использованием ключа из 1BL и рандомного набора данных из заголовка 2BL. CB_B зашифрован тем же RC4, но уже с использованием ключа процессора (нулевой, если используем сервисный CB_A), ключа шифрования CB_A и, опять же, рандомных данных из заголовка CB_B. Остальное не шифруется просто потому, что перехват управления уже выполнен и можно делать что хочется:
import struct, secrets, sys, hmac, hashlib
import Crypto.Cipher.ARC4 as RC4
key_1BL = "\xDD\x88\xAD\x0C\x9E\xD6\x69\xE7\xB5\x67\x94\xFB\x68\x56\x3E\xFA"
def encrypt_smc(data):
key = [0x42, 0x75, 0x4e, 0x79]
res = bytearray()
for i in range(len(data)):
j = data[i] ^ (key[i&3] & 0xFF)
mod = j * 0xFB
res += struct.pack("B", j)
key[(i+1)&3] += mod
key[(i+2)&3] += mod >> 8
return bytes(res)
def encrypt_cba(cba):
rnd = secrets.token_bytes(16)
key = hmac.new(key_1BL, rnd, hashlib.sha1).digest()[0:0x10]
return (key, cba[0:0x10] + rnd + RC4.new(key).encrypt(cba[0x20:]))
def encrypt_cbb(cbb, cba_key, cpu_key=b"\x00"*16):
rnd = secrets.token_bytes(16)
key = hmac.new(cba_key, rnd + cpu_key, hashlib.sha1).digest()[0:0x10]
return cba[0:0x10] + rnd + RC4.new(key).encrypt(cba[0x20:])
def insert(image, data, offset=None):
if offset is None:
offset = len(image)
if offset > len(image):
image += b"\xFF" * (offset - len(image))
return image[:offset] + data + image[offset + len(data):]
# SMC
smc = open("smc.bin", "rb").read()
smc_ptr = 0x800
# BLs
cba_ptr = 0x8000
cba = open("cba.bin", "rb").read()
cbx = open("cbx.bin", "rb").read()
cbb = open("cbb.bin", "rb").read()
cd = open("cd.bin", "rb").read()
# make header
image = struct.pack(">HHLLL64s40xLL", 0xFF4F, 1888, 0, cba_ptr, 0, b"RGH3", len(smc), smc_ptr)
# add SMC
image = insert(image, crypt_smc(smc), smc_ptr)
# add BLs
(key, cba_enc) = encrypt_cba(cba)
image = insert(image, cba_enc, cba_ptr)
image = insert(image, encrypt_cbb(cbx, key)) # MFG cba, so zero cpu_key
image = insert(image, cbb) # decrypted due to load via cbx
image = insert(image, cd) # decrypted due to patched cbb
# add XeLL
xell = open("xell.bin", "rb").read()
image = insert(image, xell, 0xC0000)
image = insert(image, xell, 0x100000)
# save image
open("image.bin", "wb").write(image)
В результате будет получен готовый для глитчинга образ (без ECC-сумм, но это легко добавляется через утилиту nandpro). Осталось записать его в приставку и… Подождите-ка, я что-то забыл. Ах да, реализовать всю основную логику глитч-хака в SMC. Расскажу, наконец, об этом.
Поскольку изнутри SMC-кода придется следить за сигналами POST-шины, а еще рулить замедлением CPU через PLL_BYPASS, нужно задействовать два дополнительных GPIO. Как POST_OUT, так и PLL_BYPASS — сигналы low voltage (1,8 В на Slim, 1,2 В на Fat), поэтому хотелось бы использовать низковольтный порт (SMC_P0_GPIO). К счастью, на Corona на нем есть два свободных пина.
GPIO5 вообще не используется, а GPIO4 переключает некоторую конфигурацию, это можно и в коде пропатчить, тем самым освободив себе пин. Поэтому для POST_OUT я использовал DB3R3, а для PLL_BYPASS — DB3R4.
На других материнках не все так радужно: все пины используются для довольно важных дел.
Если PLL_BYPASS еще можно прикрутить к 3.3v выводу DBG_LED0 (даже если припаять без резистора, это просадит порт I/O SMC, но жить будет), то POST_OUT обязательно должен быть низковольтным, иначе входной сигнал увидеть не удастся (конечно, можно сделать через транзистор или преобразователь уровней, но сами знаете: чем проще, тем лучше).
Из низковольтных GPIO входной только один — GPU_RST_DONE, по которому SMC определяет, ожил ли GPU после подачи питания. Что поделать, придется задействовать его и пропатчить SMC, чтобы он считал, что GPU всегда в порядке. В итоге DBG_LED0 я пустил на PLL_BYPASS (через резистор), а POST_OUT соединил напрямую c P0_GPIO7 до резистора на GPU_RST_DONE. Так и выпаивать ничего не нужно, и сигнал можно заменить на свой.
Для Fat-приставок, где получается, что на вход 1,8 В идет сигнал 1,2 В, лучше немного сместить уровни последовательно установленным резистором на 470 Ом. Благодаря «подтяжке» от сигнала с GPU как нижний, так и верхний уровни немного поднимутся, что повысит стабильность распознавания сигнала (да, здесь напрашивается нормальный преобразователь уровней в этом месте, но опять же — чем проще, тем лучше).
Понадобится немало патчей, чтобы прошивка заработала как полагается. Например, для хранения некоторых своих данных нужны свободные ячейки памяти. В архитектуре Intel 8051 есть несколько видов памяти и доступов к ним:
iRAM — оперативная память, 256 байт;
mov A, XXh — прямое обращение к iRAM, только для первых 128 байт;
mov A, @R0, непрямое обращение к iRAM по индексу;
CODE — область кода, 65 536 байт;
movc A, @DPTR — непрямое чтение кода;
xRAM — внешняя память (не обязательно ОЗУ), область 65 536 байт;
movx A, @R0 — непрямое чтение xRAM (8-битный адрес);
movx A, @DPTR — непрямое чтение xRAM (16-битный адрес).
В нашем случае нужна iRAM, поэтому я отжал позаимствовал ее у стека:
smc = patch(smc, 0x7E5, b'\xC2') # own bytes BF..C2 at init
smc = patch(smc, 0x804, b'\xC2') # own bytes BF..C2 at main
Немного магии. Чтобы вызываться максимально часто, при этом не мешая работать основному коду, можно встроиться в каждую процедуру в main() и заменить их на вызовы своего обработчика:
for pos in range(0x805, 0x84d, 3):
if pos == 0x829: # remove dbg LED FSM
smc = patch(smc, pos, b"\x00" * 3)
continue
my_call = lcall(CODE_START) + ljmp(orig_addr)
rom_end -= len(my_call)
smc = patch(smc, rom_end, my_call)
smc = patch(smc, pos, lcall(rom_end))
Отдельно нужно добавить код инициализации своих данных с помощью перехвата самой последней функции перед главным циклом. Сам код можно разместить в месте, где находится та самая выкушенная удаленная FSM для светодиода (также важно не забыть в конце перейти к функции, которая была заменена):
smc = patch (smc, INIT_START, init_bin + ljmp(0x293C))
smc = patch (smc, 0x7FD, lcall(INIT_START))
Остались патчи по GPIO — нужно поменять один пин с IN на OUT и пропатчить место, где он читался:
smc = patch(smc, 0x256B, b'\x90') # P0.5 OUT dir
smc = patch(smc, 0x2539, b'\xC2') # force EXT_JTM to 0
Теперь можно перейти к написанию самого кода!
Чтобы замедлить процессор, нужно отправить команду по I2C и убедиться, что она успешно выполнилась. В своем проекте CR4 XL команда Team Xecuter частично использовала код прошивки SMC, в связи с чем в блоке добавленного ими кода появилась следующая последовательность вызовов уже существующих функций:
заполнение параметров (куда, кому и что отправлять);
ожидание завершения предыдущих транзакций;
сброс параметров I2C-пинов;
проверка, что линия I2C не занята передачей;
запуск аппаратной передачи данных.
Выглядит несложно, но это костыль. Как же оно должно работать на самом деле? Давайте разбираться и делать правильно свой костыль.
Отправка I2C в прошивке разбита на три подсистемы:
конечный автомат, принимающий запросы от других частей прошивки;
виртуальная машина для подготовки последовательности запросов;
прерывание I2C.
Первая часть очень простая: как только где-нибудь в прошивке устанавливается один из флажков «я скучаю, давай общаться по аське айтусишке» (таких флажков 10), FSM задает указатель на старт байт-кода, пинает виртуальную машину, а сама тем временем (если все хорошо) переходит в уникальное состояние ожидания.
Вот и сам байт-код, точнее его часть, которая будет выполнена в этой ветке.
Или, если разбить по командам:
0x00 — инициализировать I2C-шину;
0x0E — записать HANA (0xD9 = [00 03 80 00]);
0x0E — записать HANA (0xDC = [00 00 01 E2]);
0x0E — записать HANA (0xDB = [01 E2 01 E2]);
0x0E — записать HANA (0xCD = [00 00 00 67]);
0x0B — обнулить HANA (0xDF = [00 00 00 00]);
0x03 — освободить шину I2C.
Виртуальная машина раскидывает параметры (данные, адреса) из своего же байт-кода по ячейкам памяти, ставит флажок RAM_2Dh.1 и запускает аппаратный таймер. После этого периодически (с частотой 100 КГц) возникает прерывание, во время которого из этих ячеек данные извлекаются и побитово отправляются на пины I2C. В конце концов данные заканчиваются, флажок RAM_2Dh.1 сбрасывается, таймер выключается, виртуальная машина идет на следующий шаг. Цикл продолжается, пока не достигнет кода завершения.
Короче, все, что Team Xecuter накодили, здесь делается автоматически. Как бы использовать эту систему для своих целей? Нужно добавить свой байт-код! В качестве начального вектора для виртуальной машины можно задать 1 байт от 00h до FFh, в оригинальной прошивке максимально используемая ячейка — E2h. Для этого нужно 1 (Init) + 6 (Write) + 1 (Exit) = 8 байт на каждую из двух посылок, итого 16 байт. Значит, реально дописать свое, ничего не сломав. Правда, после байт-кода располагается парочка функций, но их можно немного переместить. Главное не забыть не только переместить функции, но и пропатчить места, где они вызывались:
# i2c re-arrange
smc = patch(smc, rom_end - 0x10, smc[0x2e49:0x2e59])
rom_end -= 0x10
smc = patch(smc, 0x2E9A, lcall(rom_end))
smc = patch(smc, 0x2EA0, lcall(rom_end + 0xA))
Вот теперь вместо них можно поместить две записи нового байт-кода для ускорения и замедления процессора:
# IN W DB=[01 F0 01 Fx] EX
fast_data = b"\x00\x0E\xDB\x01\xF0\x01\xF0\x03"
slow_data = b"\x00\x0E\xDB\x01\xF0\x01\xF8\x03")
smc = patch(smc, 0x2e49, fast_data + slow_data)
Теперь для того, чтобы ускорить или замедлить процессор, нужно всего ничего: задать начальный вектор и дать сигнал виртуальной машине (поскольку это делается извне I2C FSM, нужно предварительно проверить, что FSM ничем не занята):
...
mov R0, #SMC_I2C_STATE
cjne @R0, #0, return ; check smc i2c FSM, must be idle
mov 79h, #0E3h ; fast sequence
jс skip_slow
mov 79h, #0EBh ; slow sequence
skip_slow:
lcall startup_i2c ; initiate the transfer
jb 0D0h.5, return ; failed to execute the i2c command
mov R0, #SMC_I2C_STATE
mov @R0, 4 ; move i2c FSM to the waiting state
...
В случае с Fat-ревизиями используются чуть другие константы команд, а еще нужно не только замедлять процессор, но и ускорять южный мост, поэтому и байт-код длиннее, и перетаскивать нужно больше, но в целом все очень похоже:
# i2c re-arrange
CUT_START = 0x2a38
CUT_END = 0x2a62
CUT_LEN = CUT_END - CUT_START
smc = patch(smc, rom_end - CUT_LEN, smc[CUT_START:CUT_END])
rom_end -= CUT_LEN
# delay
for off in [0x2681, 0x2687, 0x26C9, 0x26CE, 0x2A67, 0x2A6C]:
smc = patch(smc, off, lcall(rom_end))
# line status
smc = patch(smc, 0x26BB, lcall(rom_end + 0x10))
smc = patch(smc, 0x2A6F, lcall(rom_end + 0x10))
# hw trigger
smc = patch(smc, 0x26D1, lcall(rom_end + 0x14))
# IN W CE=[x8 E8 40 14] W D4=[09 90 e0 xx] EX
slow_data = b"\x00\x0B\xCE\x28\xE8\x40\x14\x0B\xD4\x09\x90\xE0\x1e\x03"
fast_data = b"\x00\x0B\xCE\x08\xE8\x40\x14\x0B\xD4\x09\x90\xE0\x0E\x03"
smc = patch(smc, CUT_START, slow_data + fast_data)
Глитчинг целиком и полностью завязан на измерении времени, и если высокоточные задержки можно выполнять просто в цикле вида while(cnt -= 1), то ожидание более длительных событий было бы неплохо измерять уже в миллисекундах. Например, близко подобраться к POST-коду, подождать устаканивания замедления и т.д.
И в SMC есть такой механизм! Помните, в main-цикле часть функций выполнялась с периодичностью в 20 мс? Такая точность достигается за счет недо-RTC (почему «недо»? потому что не умеет тикать без розетки) и его прерываний, специальный аппаратный модуль «дергает» SMC раз в миллисекунду, а тот уже ведет отсчет времени.
Как раз видно, что SMC отсчитывает до 20 и идет на круг периодических функций. Чтобы не вмешиваться в само прерывание, я написал функцию, которая анализирует переменную-счетчик и выдает результат, а был ли мальчик «тик» миллисекунд (и нужно ли уменьшать какой-нибудь счетчик замера времени):
msec_passed:
mov R0, #MSEC_REG
mov A, @R0 ; previous saved ms value
orl INT_CNTRL, #01h ; disable interrupts
cjne A, SMC_MSECS, msec_differs
sjmp msec_same
msec_differs:
mov @R0, SMC_MSECS
cjne A, #015h, msec_setc
cjne @R0, #001h, msec_setc ; do not track the 15h -> 01h
sjmp msec_same
msec_setc:
setb C
msec_same:
anl INT_CNTRL, #0FEh; enable interrupts
ret
Кажется, вот теперь все готово к основному коду, который и будет заниматься реализацией глитч-атаки.
Для простоты я разбил код на две части: одна занимается замедлением I2C, другая — слежением и глитчингом процессора. В инициализации все просто: нужно задать начальное состояние будущей FSM + добавить инициализацию UART для отладки. Без ret, поскольку сразу за init будет расположен переход к замененной функции, который и сыграет роль возврата.
org 014C0h
start:
mov R0, #RAM_START
mov @R0, #000h
mov 0E9h, #0FFh ; init UART speed
end
Простейшая заготовка для точки входа:
start:
lcall i2c_fsm ; i2c slowdown related stuff
lcall rgh_fsm ; glitch & timing related stuff
return:
ret
Дальше необходимо продумать конечный автомат для I2C. Пусть в регистре состояний первый бит означает, что именно требуется сделать (1 — замедлить, 0 — ускорить), в нулевом отображается текущее состояние (1 — замедлено, 2 — ускорено), а остальные могут быть использованы под служебные нужды (ожидание операции и сохраненное значение запроса). При входе — проверка наличия незавершенной операции и проверка ее завершения. Если операция завершена, сообщить об этом изменением состояния замедления и при необходимости запустить ожидающую команду на выполнение в виртуальной машине:
i2c_fsm:
mov R0, #I2C_ST_REG
mov A, @R0
jnb I2_BIT_WAI, try_send_i2c
; waiting for completion processing
jb SMC_I2C_S, return ; not completed
mov C, I2_BIT_SAV ; move saved request
mov I2_BIT_NOW, C ; to the real state
clr I2_BIT_WAI ; clear waiting for completion
mov @R0, A ; update rgh i2c state
ret
try_send_i2c:
mov C, I2_BIT_NOW ; compare request bit and real state
jc check_if_1
jnb I2_BIT_REQ, return ; we are fast already, nothing to do
sjmp check_state
check_if_1:
jb I2_BIT_REQ, return ; we are slow already, nothing to do
check_state:
mov R0, #SMC_I2C_STATE
cjne @R0, #0, return ; check smc i2c FSM, must be idle
mov 79h, #0E3h ; VM_POS = fast sequence
jnb I2_BIT_REQ, skip_slow
mov 79h, #0EBh ; VM_POS = slow sequence
skip_slow:
lcall startup_i2c ; initiate the transfer
jb 0D0h.5, return ; failed to execute the i2c command
i2c_success:
mov R0, #SMC_I2C_STATE ; set the i2c FSM into waiting state
mov @R0, 4 ; use the state 4 as the least problematic one
mov R0, #I2C_ST_REG
mov A, @R0
setb I2_BIT_WAI ; set 'waiting for completion'
mov C, I2_BIT_REQ
mov I2_BIT_SAV, C ; save the requested speed
mov @R0, A
ret
Теперь самое интересное — конечный автомат для глитчинга. Я разбил алгоритм на девять состояний.
Начальное состояние, переход сюда выполняется, если CPU выключен. Здесь нужно выключить все виды замедления, подготовиться к ожиданию POST-кода 0x1C, от которого будет вестись отсчет (он последний из относительно долгих), а также убедиться, что все действительно выключено, прежде чем продолжать:
rgh_fsm:
mov R0, #RGH_ST_REG
mov R1, #I2C_ST_REG
mov A, @R1
jb CPU_RST, check_state_0
mov @R0, #STATE_IDLE ; reset the glitch FSM when CPU is off
check_state_0:
cjne @R0, #STATE_IDLE, check_state_1
clr CPU_PLL ; disable PLL slowdown
clr I2_BIT_REQ ; disable I2C slowdown
mov @R1, A ; update rgh i2c state
jb I2_BIT_NOW, __return ; wait till I2C done
jnb CPU_RST, __return ; don't setup anything when off
mov R1, #DELAY_REG
mov @R1, #255 ; 1C waiting delay in ms
sjmp go_next_step
Ожидание, пока команда выполнится, а замедление по HANA устаканится. Ничего сложного — отсчитывается заданное ранее число миллисекунд:
check_state_4:
cjne @R0, #STATE_WAIT_SLOW, check_state_5
sjmp wait_for_smth
Основная логика атаки сосредоточена именно в этом шаге. Здесь происходит много всего:
прекращение замедления процессора по PLL, раз замедление I2C завершено (по-хорошему на это тоже нужна проверка, но ведь можно понадеяться на вторую I2C FSM);
отключение прерываний, чтобы выполнению никто не мешал в критический момент;
предварительное задание двух значений, которые будут ожидать - задержка перед замедлением PLL и задержка перед глитч-импульсом;
отсчет нужных задержек в цикле, замедление через PLL, короткий импульс по линии reset, сброс для псины watchdog;
обратное включение прерываний, отключение замедления и ожидание результата.
check_state_5:
cjne @R0, #STATE_GLITCH, check_state_6
clr CPU_PLL ; remove PLL slowdown, cuz i2c is done
orl INT_CNTRL, #01h ; disable interrupts
wait_post_d67:
jb POSTBIT, wait_post_d67
lcall reset_watchdog
mov R2, #PLL_DELAY_0
mov R3, #PLL_DELAY_1
mov R4, #PLL_DELAY_2
mov R5, #GLI_PULSE_0
mov R6, #GLI_PULSE_1
mov R7, #GLI_PULSE_2
wait_for_slowdown:
djnz R2, wait_for_slowdown
djnz R3, wait_for_slowdown
djnz R4, wait_for_slowdown
setb CPU_PLL
lcall reset_watchdog
wait_post_d89:
jnb POSTBIT, wait_post_d89
; here is the post DA happened, final route to the glitch!
lcall reset_watchdog ; we need really much time here
wait_for_reset: ; wait for 145.41 ms
djnz R5, wait_for_reset
djnz R6, wait_for_reset
djnz R7, wait_for_reset
clr CPU_RST ; reset pulse
setb CPU_RST
lcall reset_watchdog
clr CPU_PLL
anl INT_CNTRL, #0FEh ; enable interrupts
clr I2_BIT_REQ ; disable I2C slowdown
mov @R1, A ; update rgh i2c state
mov R1, #DELAY_REG
mov @R1, #10 ; about 10 ms to check the glitch result
sjmp go_next_step
Ожидание успеха глитча. Здесь требуется как можно точнее попасть во временной промежуток, когда кастомный загрузчик взводит POST-сигнал, сообщая, что взлом успешно выполнен. Снова простое ожидание заданного ранее числа миллисекунд.
check_state_6:
cjne @R0, #STATE_WAIT_SUCC, check_state_7
sjmp wait_for_smth
Здесь проверяется POST-сигнал 0x54 от загрузчика CB_X. Если не вышло — выполняется перезагрузка приставки.
check_state_7:
cjne @R0, #STATE_TEST_SUCC, check_state_8
mov R1, #DELAY_REG
mov @R1, #00 ; 256 ms to check the hardware init result
setb SMC_ARG_E ; enable argon processing
jnb POSTBIT, go_next_step
go_reset:
mov @R0, #STATE_FINISH ; set halt step to avoid multiple resets
ljmp prepare_reset
Последняя проверка, что загрузчик успешно инициировал оборудование (полезно для Fat-приставок). Проверяется по поднятому POST_OUT_1 пину примерно через 200 мс:
check_state_8:
cjne @R0, #STATE_WAIT_HW, check_state_9
sjmp wait_for_smth ; go wait
check_state_9:
cjne @R0, #STATE_TEST_HW, _return
jb POSTBIT, go_next_step
sjmp go_reset
Все значения задержек были подобраны экспериментальным образом с использованием логического анализатора. Глитч-тайминги были подобраны банальным перебором значений. Я примерно прикинул оценил, где должен располагаться импульс (на основе прошлых исследований), закодил брутфорс, оставил на несколько дней и посмотрел результаты по логам UART.
В самом начале я сказал, что к прошивке NAND в приставку мы еще вернемся. Так вот. XDK Sidecar записывает флешку приставки по интерфейсу SPI, манипулируя через него несколькими аппаратными регистрами. Протокол изучили, отреверсили и воспроизвели еще в 2006 году. Например, ниже — реализация стирания блока флешки.
int xbox_nand_erase_block(uint32_t lba)
{
xbox_nand_clear_status();
spiex_write_reg(0x00, spiex_read_reg(0x00) | 0x08);
spiex_write_reg(0x0C, lba << 9);
spiex_write_reg(0x08, 0xAA);
spiex_write_reg(0x08, 0x55);
spiex_write_reg(0x08, 0x05);
if (xbox_nand_wait_ready(0x1000))
return 0x8000 | xbox_nand_get_status();
return 0;
}
Когда дошли до взаимодействия с NAND уже из-под самой системы, быстро выяснилось, что интерфейс — тот же самый. Только обращения выполняются уже не по SPI, а через PCIe на южный мост.
int sfcx_erase_block(int address)
{
sfcx_writereg(SFCX_CONFIG, sfcx_readreg(SFCX_CONFIG) | CFG_WP_EN);
sfcx_writereg(SFCX_ADDRESS, address);
while (sfcx_readreg(SFCX_STATUS) & STATUS_BUSY);
sfcx_writereg(SFCX_COMMAND, UNLOCK_CMD_1);
sfcx_writereg(SFCX_COMMAND, UNLOCK_CMD_0);
while (sfcx_readreg(SFCX_STATUS) & STATUS_BUSY);
sfcx_writereg(SFCX_COMMAND, BLOCK_ERASE);
while (sfcx_readreg(SFCX_STATUS) & STATUS_BUSY);
int status = sfcx_readreg(SFCX_STATUS);
sfcx_writereg(SFCX_CONFIG,sfcx_readreg(SFCX_CONFIG) & ~CFG_WP_EN);
return status;
}
Благодаря этому в разрабатываемый сообществом загрузчик XeLL была добавлена возможность восстановить приставку из состояния «брика» (запись образа NAND с USB-носителя):
Но потом вышли материнки Corona с eMMC вместо NAND.
Уже известный протокол для доступа к ним не подходил. Вместо этого чтение выполняли подключившись картридером напрямую к отладочным площадкам самой eMMC.
Конечно же, отвалилась и возможность «анбрика» таких приставок из XeLL — заниматься реверс-инжинирингом всего этого было банально некому. Да, вы все правильно поняли: сегодня, спустя почти 12 лет, будет решена и эта несправедливость!
Если попытаться прочитать те же регистры по SPI в eMMC-режиме, то можно увидеть, что набор регистров совершенно другой и их содержимое совершенно не бьется с тем, что было при чтении NAND:
0x00: 0xC0462002 <= config? |
Поэтому придется лезть в реверс ядра системы. Проще всего взять готовое ядро с символьной информацией из слитого XDK, в нашем случае это архив 21256.18_Xenon_Recovery_with_Symbols.
Если быстро пробежаться по функциям, то можно найти самое интересное — конечный метод, посылающий команды на eMMC как раз-таки через регистры южного моста.
А еще целый конечный автомат для перезагрузки eMMC (MmcxContinueResetStateMachine).
Из анализа кода (и дальнейших исследований) можно узнать назначение некоторых регистров:
0x04 — размер блока данных;
0x08 — параметр, передаваемый в MMC-команде;
0x0C — командный регистр (номер команды и параметры выполнения);
0x10..0x1C — данные ответа по линии CMD;
0x20 — FIFO буфера данных (чтение и отправка по DAT-линиям);
0x24 — вероятно, статус выполнения команд;
0x2C — управление питанием (контроллером) eMMC;
0x30 — статус прерываний;
0x3C — вероятно, статус инициализации.
Кроме того, можно узнать конфигурацию регистров для некоторых команд, которые умеет слать система:
GO_IDLE:
[0x08] = 0
[0x0C] = 0x1800
SET_BLOCK_COUNT:
[0x04] = 0x0
[0x08] = count
[0x0C] = 0x171A0010
SELECT_CARD:
[0x04] = 0x200
[0x08] = 0xffff0000
[0x0C] = 0x71a0000
DESELECT_CARD:
[0x04] = 0x200
[0x08] = 0x0
[0x0C] = 0x7000000
А также READ_MULTIPLE и WRITE_MULTIPLE, реализованные с использованием DMA режима. Этот режим, увы, не подходит, и не только из-за того, что доступа к оперативной памяти со стороны SPI интерфейса нет и получить считанные данные будет невозможно, но и потому что регистры, отвечающие за конфигурацию DMA (0x58 / 0x5C), недоступны (по SPI наибольший доступный регистр — 0x3C).
Зато из процедуры настройки можно примерно понять, как конфигурируется регистр 0x0C; далее битовой маской указаны конкретные биты и их назначение:
0xFF000000 — номер команды MMC;
0x180000 — биты, выставляемые для выполнения команды;
0x000010 — направление чтения (1 — read, 0 — write);
0x030000 — тип команды;
0x200000 — использовать буфер передачи;
0x000001 — использовать DMA;
0x000004 — ожидать заполнения буфера записи перед подачей команды;
0x000022 — что-то про чтение или запись.
А еще информацию можно почерпнуть у самого южного моста! При запуске он самостоятельно производит инициализацию eMMC.
И что бы вы думали? Все команды, что он посылает, можно прочитать из SPI. Поэтому я сделал небольшой проект, считывающий в цикле значения регистров, и много раз перезапускал приставку, пытаясь поймать все изменения в этих регистрах. Вот такие значения 0x0C удалось поймать (упорядочены в порядке использования):
0x1 02 0000
0x2 01 0000
0x3 1a 0000
0x7 1a 0000
0x6 1b 0000
0x8 3a 0010
0x6 1b 0000
0x7 00 0000
0x6 1a 0000
x10 1a 0000
0x3 01 0000
А вот значения для регистра 0x08 (в порядке использования):
0x40ff8000 <= аргумент cmd1, send_op_cond
0x0
0xffff0000 <= аргумент cmd3, send relative addr
0x3b90100 <= аргумент cmd6, switch func
0x0
0x200 <= аргумент cmd16, set blocklen
0x3bb0000
0x3b70200
0x0
Аналогичным образом были перехвачены и другие регистры, но там не оказалось ничего интересного. На основе результатов анализа удалось реализовать достаточно полный набор MMC команд для чтения и записи eMMC по SPI. Покажу самые важные:
def read_csd():
spi_reg_write(0x04, 0x00)
spi_reg_write(0x08, 0xffff0000)
spi_reg_write(0x0C, 0xA010000) # 0x9010000 for CID
wait_simple_int()
data = b""
for i in range(0x10, 0x20, 4):
data += struct.pack("<I", spi_reg_read(i))
return data
def read_block(block):
select_card()
set_blocklen(0x200)
spi_reg_write(0x04, 0x200 | (1 << 16))
spi_reg_write(0x08, block << 9)
spi_reg_write(0x0C, 0x113a0010) #0x83A0010 for EXT_CSD
wait_int(1)
wait_int(0x20)
data = b""
for i in range(0x80):
data += struct.pack("<I", spi_reg_read(0x20))
deselect_card()
return data
def write_block(block, data):
select_card()
set_blocklen()
spi_reg_write(0x04, 0x200 | (1 << 16))
spi_reg_write(0x08, block << 9)
spi_reg_write(0x0C, 0x183a0000)
wait_int(1)
for i in range(0, 0x80):
spi_reg_write(0x20, struct.unpack("<I", data[i*4:i*4+4])[0])
wait_int(0x10)
wait_int(2)
deselect_card()
Теперь осталось это закодить на C в проект программатора и можно пользоваться. Больше никаких проблем с подпайкой картридера. А еще можно наконец-то сделать XeLL и утилиту rawflash с поддержкой «анбрика» eMMC. Красота!
На Slim (Corona или Trinity) запуск очень стабильный — практически всегда с первого раза. На некоторых приставках Jasper и Falcon запуск может длиться около минуты, но в большинстве случаев работает стабильно. И никаких чипов!
В релизе на GitHub можно найти:
исходники и собранные файлы RGH3 XeLL (загрузчик Linux / Homebrew);
образы glitch2m (полноценная система с подменой фьюзов — залил и работает);
проект программатора на базе Raspberry Pi Pico с поддержкой NAND и eMMC;
модифицированные libxenon и xell-reloaded с поддержкой записи eMMC.
Для Zephyr и Xenon, к сожалению, метод замедления с помощью PLL часто приводит к зависанию и сильно зависит от процессора и удачи. К тому же, эти ревизии приставок очень ненадежные и в живых их осталось мало, так что в релизе не участвуют
Что до консолей ревизии Winchester — они неуязвимы к RGH, а магазинные версии еще и имеют заблокированные на аппаратном уровне POST_OUT, CPU_EXT_CLK и CPU_PLL_BYPASS. Их исследование — тема для отдельной статьи 🙂