• XSS.stack #1 – первый литературный журнал от юзеров форума

Статья Reversing the AMD Secure Processor (PSP) - Part 2: Cryptographic Co-Processor (CCP)

samarie

HDD-drive
Пользователь
Регистрация
06.04.2023
Сообщения
34
Реакции
83
Депозит
0.00
Первая часть: https://xss.pro/threads/86544/
Оригинальная статья: https://dayzerosec.com/blog/2023/04...sp-part-2-cryptographic-co-processor-ccp.html
Переведенно специально для xss.pro


Это продолжение моей предыдущей статьи о процессоре AMD Secure Processor (ранее известном как Platform Security Processor или "PSP"). В той заметке я упомянул, что криптографический сопроцессор (CCP) является важным компонентом функционирования PSP. В первую очередь он отвечает за криптографию с аппаратным ускорением, но также используется в качестве механизма копирования с прямым доступом к памяти (DMA) для выполнения операций массового копирования, которые включают загрузку и распаковку микропрограмм. Со временем CCP развивался и включал в себя все больше и больше функций. В этом посте мы будем говорить о последней версии на момент написания статьи, CCPv5.

Несмотря на то, что CCP является проприетарным и в основном недокументированным блоком интеллектуальной собственности (IP), некоторая публичная информация существует благодаря драйверу CCP с открытым исходным кодом ядра Linux [1]. Он реализует интерфейс для передачи заданий из ядра в PSP Secure OS, которые затем передаются в CCP. В основе интерфейса CCP лежат локальные блоки хранения и очереди команд для отправки заданий.

Local Storage Blocks​

Подобно Syshub и Сети управления системой (SMN), CCP опирается на концепцию слотов для сохранения контекста при выполнении различных операций. Эти слоты содержатся внутри локальных блоков хранения или "LSBs". LSBs являются развитием блоков хранения CCPv3, которые представляют собой блоки памяти, локальные для CCP. В CCPv3 эти блоки можно было использовать только для хранения ключей с ограниченной инициализацией, но в v5 они более универсальны. Я полагаю, что вы можете шифровать и расшифровывать непосредственно в LSBs и через LSBs, что может позволить вам сделать несколько классных безопасных производных ключей без того, чтобы чувствительная информация вообще покидала эти LSBs.

Заголовки драйвера ядра Linux могут дать нам много информации о том, как эти блоки памяти делятся и используются.


Код:
#define MAX_LSB_CNT                 8

#define LSB_SIZE                    16
#define LSB_ITEM_SIZE               32
#define PLSB_MAP_SIZE               (LSB_SIZE)
#define SLSB_MAP_SIZE               (MAX_LSB_CNT * LSB_SIZE)

#define LSB_ENTRY_NUMBER(LSB_ADDR)  (LSB_ADDR / LSB_ITEM_SIZE)


Существует максимум 8 LSB. Один LSB может содержать 16 слотов, в каждом из которых может храниться 32 байта данных. Это дает в общей сложности 512 байт на LSB, или 4 КБ общей памяти. Команды, посылаемые в ЦПУ, используют виртуальную адресацию для доступа к LSB.

01a430f81dea7e6379b2b.png

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

Command queues​

Как уже говорилось ранее, всего существует пять очередей команд, которые могут использоваться для подачи в CCP. Каждая очередь может содержать 16 команд, которые состоят из восьми 32-битных двойных слов или 256 байт. Опять же, в заголовке дается приличное описание того, как описываются команды [3].
Код:
/**
 * descriptor for version 5 CPP commands
 * 8 32-bit words:
 * word 0: function; engine; control bits
 * word 1: length of source data
 * word 2: low 32 bits of source pointer
 * word 3: upper 16 bits of source pointer; source memory type
 * word 4: low 32 bits of destination pointer
 * word 5: upper 16 bits of destination pointer; destination memory type
 * word 6: low 32 bits of key pointer
 * word 7: upper 16 bits of key pointer; key memory type
 */
// ...

Определения структур и связанных с ними макросов можно найти в заголовке устройства CCP [3].

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

Основной опкод, используемый для диспетчеризации, поступает из управляющего слова в dword 0, а именно из битов функции в 15:31.

Код:
enum ccp_engine {
 CCP_ENGINE_AES = 0,
 CCP_ENGINE_XTS_AES_128,
 CCP_ENGINE_DES3,
 CCP_ENGINE_SHA,
 CCP_ENGINE_RSA,
 CCP_ENGINE_PASSTHRU,
 CCP_ENGINE_ZLIB_DECOMPRESS,
 CCP_ENGINE_ECC,
 CCP_ENGINE__LAST,
};

Это перечисление дает нам представление о том, сколько различных механизмов и режимов может поддерживать CCP, от декомпрессии до симметричного и асимметричного крипто и хэширования.

Вы часто будете видеть ссылки на "тип" для источника, назначения и ключей. Это очень важно иметь в виду, поскольку CCP может выполнять ввод-вывод в различные типы памяти.

Код:
enum ccp_memtype {
 CCP_MEMTYPE_SYSTEM = 0,
 CCP_MEMTYPE_SB,
 CCP_MEMTYPE_LOCAL,
 CCP_MEMTYPE__LAST,
};


"Системная" память относится к DRAM / памяти, доступной для x86. Как вы, вероятно, догадываетесь, "SB" относится к памяти LSB, а "Local" - к локальному статическому ОЗУ (SRAM) PSP. Во внечиповом загрузчике начальных программ (IPL) PSP часто используются SB и Local типы памяти. Хотя адреса локальной и SB памяти умещаются в 32 битах, этого недостаточно для физических адресов DRAM, поэтому дескрипторы поддерживают 48-битные адреса.

Загрузка микропрограммы​

Отходя от драйвера ядра, давайте вернемся к бинарному файлу PSP IPL. Просмотрев строки, я обнаружил функцию spiRead(), которая используется для чтения данных с флэш-памяти SPI.

df4df04f3785312c2e141.png

Как видно из перевернутых аргументов, он может поддерживать как сжатое, так и несжатое чтение. Если размер данных меньше 1 КБ, будет использован стандартный вызов memcpy(), в противном случае будет вызвана ccp_passthrough(). Вы заметите, что в любом случае копирование происходит в/из одного и того же типа памяти (локальной), поскольку CCP рассматривает адреса SMN/Syshub как локальную память PSP.


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


Код:
void ccp_mmio_write(struct ccp_mmio_req* req, int queue_idx, int a3)
{
 uint32_t* mmio_reg = (uint32_t*) (0x3001000 + (queue_idx * 0x1000));

 do {
  // busy wait on queue to be free
 } while (mmio_req[0] << 0x1F)
 // ...

 mmio_req[0] = req->ctrl;
 mmio_req[1] = req->tail;
 mmio_req[2] = req->head;
}

int ccp_passthrough(void* src, void* dest, uint32_t size, int src_type, int dest_type, int a6)
{
 struct ccp_passthrough_req req;
 struct mmio_ccp_req mmio_req;

 if (src == NULL || dest == NULL)
  return BL_ERR_INVALID_PARAMETER;

 bzero(&req, sizeof(struct ccp_passthrough_req)); // size = 0x20
 bzero((void*) 0xE680, 0x80);
 
 // Control
 CCP5_CMD_SOC(&req) = 1;
 CCP5_CMD_EOM(&req) = 1;
 CCP5_CMD_FUNCTION(&req) = CCP_ENGINE_PASSTHRU;

 CCP5_CMD_LEN(&req) = size;

 // Source
 if (src_type == CCP_MEMTYPE_LOCAL)
  CCP5_CMD_SRC_LO(&req) = sub_b0a0(src);
 else
  CCP5_CMD_SRC_LO(&req) = src;
 CCP5_CMD_SRC_MEM(&req) = src_type;

 // Dest
 if (dest_type == CCP_MEMTYPE_LOCAL)
  CCP5_CMD_DST_LO(&req) = sub_b0a0(dest);
 else
  CCP5_CMD_DST_LO(&req) = dest;
 CCP5_CMD_DST_MEM(&req) = dest_type;

 // Copy and submit mmio write
 memcpy((void*) 0xE680, &req, sizeof(struct ccp_passthrough_req));
 // ...

 mmio_req.ctrl = CMD5_Q_RUN;
 mmio_req.head = (void*) 0xE680;
 mmio_req.tail = (void*) 0xE6A0;
 // ...

 *(uint32_t*) (0x3006000) = 1;
 ccp_mmio_write(&mmio_req, queue_idx: 0, 6);
 // ...
 if (ccp_wait_status_update(queue_idx: 0))
  return BL_ERR_CCP_PASSTHR;
 return 0;
}


Здесь мы имеем довольно стандартный регистровый запрос Memory Mapped I/O (MMIO), который используется для отправки запросов CCP через ringbuffer. Каждая очередь получает область MMIO размером 0x1000 байт, первые три слова используются для управляющих битов, хвоста и заголовка соответственно. Заголовок указывает на запрос установки, а хвост - на обнуленные / NOP данные.


Насколько я видел, все запросы CCP, сделанные IPL, будут использовать очередь команд #0. Это имеет смысл, поскольку IPL относительно проста и не нуждается в использовании всех очередей. Вот диаграмма, дающая общее представление о настройке для отправки запросов в CCP:

509abc9db4f7e5be74910.png

Расшифровка прошивки​

Одной из главных раздражающих особенностей PSP (по крайней мере, с точки зрения исследователей) является возможность шифрования прошивки на флэш-памяти. В таких случаях прошивки шифруются с помощью того, что некоторые другие исследователи называют компонентным ключом (cK), который встроен в содержимое заголовка прошивки. Разумеется, этот ключ шифруется промежуточным ключом шифрования (iKEK), который также хранится на флэш-памяти и также шифруется корневым ключом (rK). Расшифровка включает в себя сначала расшифровку iKEK с помощью корневого ключа (rK), затем расшифровку cK для окончательной расшифровки открытого текста прошивки. Корневой ключ остается в заблокированном слоте CCP, и, предположительно [2], не может быть легко сброшен, даже если у вас есть выполнение кода на этом этапе.

В статье "One Glitch to Rule Them All" [2] это описано на высоком уровне, но давайте спустимся в кроличью нору к коду, ответственному за получение и расшифровку этого ключа компонента и прошивки. Он начинается в том, что я условно назвал _bootloader_enter_c_main() после завершения большей части инициализации загрузчика.


Код:
int _bootloader_enter_c_main() {
 // ...
 char wrapped_ikek[0x10] = {0};
 err = spiReadPspDirEntry(entry_id: 0x21, dest: &wrapped_ikek, size: 0x10);
 if (err) { /* ... */ }
 err = ccp_aes_ecb_decrypt(
  key: 0x80,
  key_type: CCP_MEMTYPE_SB,
  key_size: 0x10,
  src: &wrapped_ikek,
  src_type: CCP_MEMTYPE_LOCAL,
  len: 0x10,
  dest: (void*) 0xF2C0,
  dest_type: CCP_MEMTYPE_LOCAL
 );
 // ...
}


В данном случае ключ компонента хранится по адресу 0xF2C0, который будет использоваться различными функциями в IPL, когда пользовательское пространство запрашивает двоичный файл для загрузки через syscall. Вы заметите, что ключ 0x80 является адресом LSB (который разрешается в слот 4 в LSB #0). Предположительно, эта область зарезервирована и заблокирована CCP, и вы не можете просто считать ее.

И ccp_aes_ecb_decrypt(), и ccp_aes_ecb_encrypt() оборачиваются вокруг того, что я назвал ccp_aes_ecb_crypt(), и просто вызывают его с одним немного отличающимся аргументом, указывающим, является ли это операцией шифрования или расшифровки.


Код:
int ccp_aes_ecb_crypt(
 uint32_t key,
 int key_type,
 uint32_t key_size,
 uint32_t a4,
 void* src,
 int src_type,
 uint32_t size,
 void* dest,
 int dest_type,
 int a10,
 int is_encrypt) {
 int aes_type;
 int function;
 struct ccp_aes_req req;
 struct mmio_ccp_req mmio_req;

 if (src == NULL || dest == NULL || key_type == CCP_MEMTYPE_SYSTEM)
  return BL_ERR_INVALID_PARAMETER;

 switch (key_size) {
 case 0x10:
  aes_type = 0; // aes-128
  break;
 case 0x18:
  aes_type = 1; // aes-192
  break;
 case 0x20:
  aes_type = 2; // aes-256
  break;
 default:
  return BL_ERR_INVALID_PARAMETER;
 }

 bzero(&req, sizeof(struct ccp_aes_req)); // size = 0x20
 bzero((void*) 0xE680, 0x80);

 // Control
 CCP_AES_ENCRYPT(&req) = is_encrypt;
 CCP_AES_MODE(&req) = CCP_AES_MODE_ECB;
 CCP_AES_TYPE(&req) = aes_type;

 CCP5_CMD_SOC(&req) = 1;
 CCP5_CMD_EOM(&req) = 1;
 CCP5_CMD_LEN(&req) = size;

 // Key
 if (key_type == CCP_MEMTYPE_LOCAL)
  CCP5_CMD_KEY_LO(&req) = sub_b0a0(key);
 else
  CCP5_CMD_KEY_LO(&req) = key;
 CCP5_CMD_KEY_MEM(&req) = key_type;

 // Source
 if (src_type == CCP_MEMTYPE_LOCAL)
  CCP5_CMD_SRC_LO(&req) = sub_b0a0(src);
 else
  CCP5_CMD_SRC_LO(&req) = src;
 CCP5_CMD_SRC_MEM(&req) = src_type;

 // Dest
 if (dest_type == CCP_MEMTYPE_LOCAL)
  CCP5_CMD_DST_LO(&req) = sub_b0a0(dest);
 else
  CCP5_CMD_DST_LO(&req) = dest;
 CCP5_CMD_DST_MEM(&req) = dest_type;

 // Copy and submit mmio write
 memcpy((void*) 0xE680, &req, sizeof(struct ccp_passthrough_req));
 // ...

 mmio_req.ctrl = CMD5_Q_RUN;
 mmio_req.head = (void*) 0xE680;
 mmio_req.tail = (void*) 0xE6A0;
 // ...

 ccp_mmio_write(&mmio_req, queue_idx: 0, 6);
 // ...
 if (ccp_wait_status_update(queue_idx: 0))
  return BL_ERR_CCP_AES;
 return 0;
}


Глядя на эту функцию, мы видим, что она в некоторой степени похожа на DMA copy passthrough, но с гораздо большим контекстом, где операции AES более сложны. Большинство этих обработчиков CCP выглядят довольно похоже, используя соответствующие макросы для инициализации управляющих битов и установки дескриптора запроса, поэтому я не буду подробно рассматривать их все.


Теперь, когда IKEK расшифрован и хранится в памяти, мы можем посмотреть, где он используется для расшифровки прошивки. Обработчик системного вызова для SVC_ENTER (который загружает блоб прошивки из файловой системы прошивки) в конечном итоге вызовет то, что я назвал fw_copy(). Обратите внимание, что srcdest содержит как уже загруженные исходные данные, так и место, куда следует записать окончательно сформированное содержимое.

Код:
int component_key_decrypt(char* enc_key, char* dec_key) {
 return ccp_aes_ebc_crypt(
  key: (void*) 0xF2C0, key_type: CCP_MEMTYPE_LOCAL, key_size: 0x10,
  src: &enc_key, src_type: CCP_MEMTYPE_LOCAL, size: 0x10,
  dest: dec_key, dest_type: CCP_MEMTYPE_LOCAL
 );
}

int fw_body_decrypt(
 char* key,
 int key_type,
 uint32_t key_size,
 char* iv,
 char* src,
 int src_type,
 uint32_t size,
 char* dest,
 int dest_type) {
 return ccp_aes_cbc_crypt(
  key, key_type, key_size,
  iv, src, src_type, size,
  dest, dest_type, 1, is_encrypt: 0);
}

int fw_copy(char* srcdest, /* ... */) {
 int err;
 char enc_component_key[0x10];
 char component_key[0x10];
 // ...
 if (srcdest[0x18] == 1) {                               // PSP header 'is_encrypted'
  memcpy(&enc_encomponent_key, &srcdest[0x80], 0x10); // PSP header 'wrapped_key'
  err = component_key_decrypt(&enc_encomponent_key, &component_key);
  // ...
  err = fw_body_decrypt(
   key: &component_key,
   key_type: CCP_MEMTYPE_LOCAL,
   key_size: 0x10,
   iv: &srcdest[0x20],                             // PSP header 'iv'
   src: &srcdest[0x100],
   src_type: CCP_MEMTYPE_LOCAL,
   size: (uint32_t) (srcdest[0x14]),               // PSP header 'body_size'
   dest: &srcdest[0x100],
   dest_type: CCP_MEMTYPE_LOCAL
  );
 }
}

Как и ожидалось, fw_copy() сначала получит и расшифрует компонентный ключ, получит другую информацию из заголовка файла PSP (например, IV) и расшифрует тело файла с помощью AES-128 CBC.

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

Выводы​

Хотя PSP является жизненно важным компонентом современных систем AMD, интерфейс к нему не слишком сложен, если его разобрать. Похоже, что PSP использует практически те же структуры запросов и идентификаторы, что и Secure OS для ядра x86, что вполне справедливо, поскольку нет смысла изобретать колесо без необходимости. Я думаю, что тот факт, что она может поддерживать три различных типа памяти (PSP SRAM/local, локальные блоки хранения и системную DRAM), довольно крут, и вы можете получить некоторые интересные взаимодействия между типами памяти. Он также поддерживает множество различных типов операций, которые являются фундаментальными для различных систем шифрования и выведения ключей.


Наконец, CCP хранит некоторые собственные секреты, непрозрачные даже для самой PSP, например, корневой ключ. Мне было бы интересно узнать, как именно реализован этот механизм блокировки, однако это уже переходит на территорию аппаратного обеспечения, в котором я, честно говоря, не очень хорошо разбираюсь. Однако мне интересно, насколько концепция блокировки слотов и сохранения их исключительно для подмножества очередей команд используется в целях глубокой обороны. Могут быть очень интересные возможности для исследований, если конкретная система не обеспечивает хорошую блокировку, например, утечка ключей и других конфиденциальных данных. Это потребует изучения Secure OS, хотя... возможно, мы рассмотрим это в одной из будущих статей блога!

Ресурсы​

 


Напишите ответ...
  • Вставить:
Прикрепить файлы
Верх