
Exploits de prova de conceito para três vulnerabilidades no mecanismo de atualização de firmware do SSD Crucial MX500, permitindo buffer overflows e potencial execução de código via comandos ATA.
O dispositivo em questão é qualquer SSD da série MX500. Estes SSDs são controlados por um microcontrolador Sillicon-Motion SM2259 (lotes mais antigos possuíam um controlador mais antigo, Sillicon-Motion SM2258, mas o foco principal deste documento é o mais novo).
SM2259 é um microcontrolador SATA de 4 canais a 6Gb/s que possui uma CPU little-endian de 32 bits baseada na arquitetura ARC.
Observando o firmware mais recente aplicável à data deste documento, que é M3CR046, alguns problemas foram identificados e confirmados tanto estática quanto dinamicamente.
Todos os problemas foram identificados no mecanismo de atualização de firmware do controlador, que corresponde ao manipulador do microcontrolador para o comando ATA PIO DOWNLOAD-MICROCODE (0x92), especificamente na lógica que baixa o firmware usando o método de offsets, que corresponde aos subcomandos 0x03 e 0x0E.
Todos os bugs abordados neste documento foram verificados em um Crucial MX500 500GB SSD (CT500MX500SSD1), controlador SM2259H-AC rodando FW M3CR046 com chips flash NY112, usando um PC com CPU x86_64.
O código do FW está mapeado para o endereço base 0x80020000, e o manipulador ATA vulnerável está localizado no endereço 0x80024A9C. Uma versão descompilada da função pode ser encontrada em resources/download_microcode_handler.c para sua conveniência.
Para aqueles que preferem não lidar com os detalhes técnicos e preferem entender o resultado final, consulte a seção de FAQ abaixo.
Como o M3CR046 contém múltiplas imagens de firmware, das quais a apropriada é escolhida pelo mecanismo de atualização de firmware (talvez dependendo dos chips flash reais utilizados ou de outras características de hardware), este documento cobrirá as especificidades da primeira variante de firmware (já que este é o firmware suportado em nossa unidade específica e, portanto, só pudemos testar essa variante). No entanto, os bugs apresentados neste documento parecem se aplicar a todas as variantes de firmware, mas pode haver algumas diferenças nas especificidades reais ao reproduzi-los.
Este problema refere-se a casos em que o primeiro bloco enviado tem tamanho maior que 0x200 setores. Se observarmos o manipulador do comando ATA, especificamente na lógica executada quando o tamanho do bloco é maior que o tamanho do setor e quando o bloco é o primeiro enviado:

Isso define algumas variáveis com base no próximo offset (que no nosso caso, como enviamos apenas um bloco até agora, é o comprimento do bloco em setores) e em uma variável chamada lower_bound_fw_offset, que é o offset de bloco (ou seja, offset na granularidade de setores) dentro da imagem de download de entrada onde nossa imagem de firmware é esperada. Este é um valor hardcoded por variante de firmware, que no nosso caso (a primeira variante) é igual a 0.
Neste caso, ocorre um underflow ao calcular o resultado da subtração para some_index, fazendo com que some_index atinja o valor de 0xFFFF. Este é um comportamento inesperado, pois, com base na lógica que move os dados para o buffer de download:

Observamos que o endereço de origem do qual os dados são copiados pode não ser válido, dado o valor inesperado calculado para some_index.
Ao testar isso dinamicamente, enviando uma solicitação de atualização de firmware com o primeiro bloco de tamanho maior que 0x200 setores, o controlador trava e nem sequer envia uma resposta à solicitação original. Isso é consistente e facilmente reproduzível.
É provável que isso aconteça devido a uma referência inválida ao endereço de origem calculado, que então dispara uma exceção que causa a trava do controlador. Isso não foi comprovado, mas é uma conjectura que pode explicar a trava.
A imagem de download de entrada (para M3CR046) tem tamanho 0x242400 bytes, e dentro desta imagem existem 3 imagens internas de firmware, das quais apenas uma é eventualmente gravada na flash após um processo de atualização de firmware. Cada uma dessas imagens tem tamanho 0xC0C00 bytes (ou 0x606 setores).
Isso significa que, quando o mecanismo de atualização de firmware extrai a cópia correta do firmware da imagem de download de entrada, ele deve verificar se seu tamanho não excede 0xC0C00 bytes.
O controlador realmente tenta fazer isso, mas há alguns casos extremos que podem levar a um comportamento inesperado. Vamos dar uma olhada no seguinte trecho (que compartilha algum código com o bug anterior):

Se o bloco atual tiver tamanho maior que 0x200 setores e não for o primeiro bloco na sequência, então 0x200 setores (0x40000 bytes) serão copiados de cada vez. Em seguida, há uma verificação cujo propósito é truncar os bytes excedentes do número de bytes a copiar se o tamanho total da imagem de firmware exceder higher_bound_fw_offset (que no nosso caso é 0x606 setores, já que o tamanho do firmware deve ser exatamente este).
Essa lógica faz sentido de modo geral, mas há uma falha – se o último bloco enviado fizer com que o próximo offset se torne muito alto, de modo que o número de bytes excedentes ultrapasse 0x200 setores (ou 0x40000 bytes), então curr_bytes_to_copy recebe um valor "negativo", que sofre underflow para aproximadamente ~4GB (~0xFFFFFFFF). Como vimos antes, esta variável é usada para determinar o número de bytes a serem transferidos para o buffer de download.
Se observarmos o interior de r_maybe_some_efficient_data_transfer, vemos o seguinte trecho de código:

O que significa que o tamanho da cópia é truncado para 32MB (a partir do tamanho de cópia original de ~4GB), mas ainda é um número grande que também pode causar um comportamento indefinido se o intervalo de memória começando em 0x40000000 tiver tamanho inferior a 32MB.
Ao testar isso dinamicamente, enviando blocos ATA para chegar a um offset de 0x600 e, em seguida, enviando um bloco grande de tamanho 0x207 setores para disparar o underflow, o controlador trava novamente, provavelmente devido a um acesso de memória inválido durante a cópia.
Este bug é mais interessante que o anterior, porque, embora não tenhamos uma sobrescrita controlada (mas sim uma grande sobrescrita que possivelmente dispara uma exceção que trava o controlador), se a função que move os dados para o buffer de download realmente conseguir transferir essa quantidade de dados antes de travar (sobrescrevendo o intervalo de memória localizado logo após o buffer de download na memória principal), então talvez o comportamento do manipulador de exceções possa ser alterado com base nos dados sobrescritos. Isso pode acontecer, por exemplo, se o manipulador de exceções ler um ponteiro da área sobrescrita e então pular para ele (este caso específico não é particularmente provável, mas com mais pesquisa, algo do tipo pode ser descoberto).
Conforme afirmado, o tamanho da imagem de download é 0x242400 (ou 0x1212 setores). O firmware verifica que o tamanho total da imagem transferida não excede este tamanho, verificando se o próximo offset não ultrapassa 0x1212 setores. Esta verificação faz sentido, mas o cálculo do próximo offset é falho:

Se o offset atual é 0x600 setores e o próximo comando ATA a ser processado tem tamanho grande o suficiente (digamos 0xFC00 setores, o que é permitido pelo padrão ATA), então o próximo offset sofre wrap around, de modo que a verificação mencionada não funciona adequadamente:

Ou em outras palavras, no caso normal, o mecanismo de atualização de firmware reiniciaria sua máquina de estados e retornaria um erro, mas se enviarmos um bloco muito grande, continuaríamos processando-o. O trecho de código a seguir mostra como a transferência é feita:

Recordamos neste ponto que, se o número de setores a transferir for maior que 0x200 setores e o bloco atual não for o primeiro, então 0x200 setores são copiados de cada vez para o buffer de download. Isso é muito interessante, porque significa que podemos copiar cerca de 0x200 setores (ou 0x40000 bytes) além do buffer de download, sobrescrevendo dados na memória principal. Por exemplo, se o offset atual é 0x605 setores e fornecemos um tamanho de bloco de 0xF9FB setores, então __next_offset recebe o valor de 0 devido ao wrap around. O índice de origem a partir do qual a cópia começa é 0, e curr_bytes_to_copy recebe o valor de 0x40000. Como estamos atualmente no offset 0x605 setores, então g_blocks_copied recebe o valor de 0x605. Como o offset atual é de fato válido (e o próximo também), então a operação de cópia para o buffer de download é acionada, causando uma sobrescrita massiva de pouco menos de bytes além do final do buffer de download.
Esta é uma primitiva forte que permite um estouro de buffer muito mais controlado (que não trava o controlador imediatamente como nos casos anteriores) e pode levar à execução de código com uma certeza muito maior do que o bug anterior (mas ainda assim, mais pesquisa precisa ser feita sobre o que exatamente está localizado após o buffer de download na memória principal para determinar as características da exploração).
Todos esses bugs foram verificados em uma máquina Ubuntu 22.04 64 bits usando o driver SCSI padrão do Linux sobre a interface SG_IO. Deve-se notar que para reproduzir o Bug #3 com este driver específico, páginas enormes devem estar habilitadas e uma única página de 1GB deve ser alocada para a solicitação grande. A razão para isso é que, aparentemente, este driver exige que toda a solicitação ATA esteja em blocos de memória física contígua. Como a solicitação tem tamanho próximo a ~30MB, páginas de 2MB não são suficientes, e assim páginas de 1GB são o próximo (e último) tamanho disponível em nosso sistema de teste.
No entanto, também deve ser notado que isso não significa que esta seja uma etapa necessária para desencadear isso, porque talvez existam outras alternativas que permitam enviar solicitações ATA grandes que ainda não cobrimos. Habilitar páginas enormes foi simplesmente a rota mais rápida para confirmar este bug. Além disso, o único pré-requisito necessário para desencadear todos esses bugs são as permissões necessárias para enviar pacotes ATA (tipicamente, acesso root ao PC que se comunica com o controlador).
O código fonte que reproduz todos os bugs mencionados é fornecido como parte deste repositório. Para o Bug #1 e Bug #2, o comportamento esperado é que a unidade trave até o próximo ciclo de energia. Para o Bug #3, o código fonte fornecido não necessariamente trava o controlador, mas realiza uma grande sobrescrita além do buffer de download.
Conforme afirmado, como os bugs foram verificados em uma máquina Ubuntu 22.04 64 bits, o processo de compilação deve ser feito em uma máquina similar. Não há garantias para outras distribuições ou sistemas operacionais.
Para compilar, execute o seguinte no diretório raiz do projeto:
cmake -B build && make
O processo de compilação gera 3 binários, todos disponíveis no diretório build com os nomes CVE_MX500_BUG_1, CVE_MX500_BUG_2 e CVE_MX500_BUG_3, que correspondem aos arquivos fonte que disparam o Bug #1, Bug #2 e Bug #3, respectivamente.
Cada binário espera receber o caminho do dispositivo do SSD MX500 e deve ser executado com privilégios de root. Por exemplo:
sudo ./build/CVE_MX500_BUG1 /dev/sda
Depende do objetivo final de um potencial atacante. Se tudo o que ele quer é acesso total de leitura/escrita ao armazenamento da sua unidade, então estar dentro do seu PC já é suficiente. No entanto, e se esse atacante quiser ir alguns passos além? Se o FW de uma unidade for assinado digitalmente, então o Bug #3 pode permitir que um atacante burle a verificação de assinatura do firmware, permitindo que o atacante insira uma carga maliciosa no firmware da unidade. Uma vez dentro, tal carga está muito bem escondida, sobrevive a formatações da unidade e pode até garantir sobreviver a atualizações de firmware do controlador. O que tal carga poderia realmente fazer está além do escopo deste documento, portanto não será discutido.
É provável que a resposta seja um grande NÃO. A quantidade de P&D necessária para realmente realizar tal ataque é muito alta e seria (MUITO provavelmente) possível apenas por agentes de ameaças muito sérios. A menos que você seja procurado por governos, é extremamente improvável que isso o afete de alguma forma.
O fornecedor não respondeu a múltiplos e-mails sobre esses problemas ao longo de meses. Para que um CVE seja realmente publicado, um link público deve ser fornecido ao CNA designado. Infelizmente, enviar as informações em privado não é como isso funciona.
Os bugs mencionados neste documento foram originalmente descobertos em maio de 2024. A Micron foi contatada múltiplas vezes desde então (através do seu e-mail oficial de segurança), e não houve resposta deles. A MITRE foi notificada em julho de 2024, e um CVE foi atribuído em agosto de 2024. No final de agosto de 2024, este repositório foi tornado público (alguns dias após o CVE ser aprovado pela MITRE).
Como firmwares M3CR04X anteriores ao M3CR046 não estão mais disponíveis para download, não está claro se são afetados, mas se eu tivesse que adivinhar, diria que sim. Em relação a versões ainda mais antigas, por exemplo, M3CR033, com base em análise estática, parece que bugs muito semelhantes existem lá.
O controlador em questão, SM2259, está embutido em SSDs de outros fornecedores também. É possível que os fornecedores modifiquem alguma parte do código do firmware, mas eu também diria que é definitivamente possível que esses bugs (ou outros muito semelhantes) estejam presentes em SSDs de outros fornecedores também.
Este CVE foi publicado pela MITRE. Também foi analisado pelo NVD com uma pontuação CVSS 3.0 de 6.7 (médio).
Se você identificou imprecisões ou erros na descrição ou está tendo dificuldades para reproduzir esses bugs, entre em contato comigo em [email protected].
0x40000