Nota
O acesso a esta página requer autorização. Podes tentar iniciar sessão ou mudar de diretório.
O acesso a esta página requer autorização. Podes tentar mudar de diretório.
Este artigo contém um guia para ajudar os desenvolvedores a identificar e mitigar vulnerabilidades de hardware relacionadas a canais laterais de execução especulativa no software C++. Essas vulnerabilidades podem divulgar informações confidenciais através dos limites de confiança e podem afetar o software executado em processadores que suportam a execução especulativa e fora de ordem de instruções. Essa classe de vulnerabilidades foi descrita pela primeira vez em janeiro de 2018 e informações e orientações adicionais podem ser encontradas no comunicado de segurança da Microsoft.
As orientações fornecidas por este artigo estão relacionadas com as classes de vulnerabilidades representadas por:
CVE-2017-5753, também conhecida como variante 1 do Spectre. Essa classe de vulnerabilidade de hardware está relacionada a canais laterais que podem surgir devido à execução especulativa que ocorre como resultado de uma previsão incorreta de ramificação condicional. O compilador Microsoft C++ no Visual Studio 2017 (a partir da versão 15.5.5) inclui suporte para o
/Qspectreswitch que fornece uma mitigação em tempo de compilação para um conjunto limitado de padrões de codificação potencialmente vulneráveis relacionados ao CVE-2017-5753. O/Qspectreinterruptor também está disponível no Visual Studio 2015 Atualização 3 por meio de KB 4338871. A documentação para o/Qspectresinalizador fornece mais informações sobre seus efeitos e uso.CVE-2018-3639, também conhecido como Speculative Store Bypass (SSB). Essa classe de vulnerabilidade de hardware está relacionada a canais laterais que podem surgir devido à execução especulativa de uma carga à frente de um armazenamento dependente como resultado de uma previsão incorreta de acesso à memória.
Uma introdução acessível às vulnerabilidades dos canais secundários de execução especulativa pode ser encontrada na apresentação intitulada The Case of Spectre and Meltdown por uma das equipes de pesquisa que descobriram estas vulnerabilidades.
O que são as vulnerabilidades de hardware associadas ao Canal Lateral de Execução Especulativa?
CPUs modernas fornecem maiores graus de desempenho, fazendo uso de execução especulativa e fora de ordem de instruções. Por exemplo, isso geralmente é feito prevendo o destino de ramificações (condicionais e indiretas), o que permite que a CPU comece a executar instruções especulativamente no alvo de ramificação previsto, evitando assim uma paralisação até que o alvo real da ramificação seja resolvido. No caso de a CPU descobrir mais tarde que ocorreu um erro de previsão, todo o estado da máquina que foi calculado especulativamente é descartado. Isso garante que não haja efeitos arquiteturalmente visíveis da especulação mal prevista.
Embora a execução especulativa não afete o estado arquitetonicamente visível, ela pode deixar vestígios residuais em estado não arquitetônico, como os vários caches usados pela CPU. São estes vestígios residuais de execução especulativa que podem dar origem a vulnerabilidades de canais laterais. Para entender melhor isso, considere o seguinte fragmento de código que fornece um exemplo de CVE-2017-5753 (Bounds Check Bypass):
// A pointer to a shared memory region of size 1MB (256 * 4096)
unsigned char *shared_buffer;
unsigned char ReadByte(unsigned char *buffer, unsigned int buffer_size, unsigned int untrusted_index) {
if (untrusted_index < buffer_size) {
unsigned char value = buffer[untrusted_index];
return shared_buffer[value * 4096];
}
}
Neste exemplo, ReadByte é fornecido um buffer, um tamanho de buffer e um índice nesse buffer. O parâmetro index, conforme especificado por untrusted_index, é fornecido por um contexto menos privilegiado, como um processo não administrativo. Se untrusted_index for menor que buffer_size, então o carácter nesse índice é lido do buffer e usado para indexar na região compartilhada de memória referida por shared_buffer.
De uma perspetiva arquitetônica, essa sequência de código é perfeitamente segura, pois é garantido que untrusted_index sempre será menor que buffer_size. No entanto, na presença de execução especulativa, é possível que a CPU preveja incorretamente a ramificação condicional e execute o corpo da instrução if, mesmo quando untrusted_index é maior ou igual a buffer_size. Como consequência disso, a CPU pode ler especulativamente um byte além dos limites de buffer (o que pode ser um segredo) e pode então usar esse valor de byte para calcular o endereço de uma carga subsequente através de shared_buffer.
Embora a CPU eventualmente detete essa previsão incorreta, efeitos colaterais residuais podem ser deixados no cache da CPU que revelam informações sobre o valor de byte que foi lido fora dos limites do buffer. Esses efeitos colaterais podem ser detetados por um contexto menos privilegiado em execução no sistema, investigando a rapidez com que cada linha de cache em shared_buffer é acessada. As medidas que podem ser tomadas para conseguir isso são:
Invoque
ReadBytevárias vezes comuntrusted_indexinferior abuffer_size. O contexto de ataque pode fazer com que o contexto da vítima invoqueReadByte(por exemplo, via RPC) de tal forma que o preditor de ramo seja treinado para não ser tomado comountrusted_indexé menor quebuffer_size.Elimine todas as linhas de cache em
shared_buffer. O contexto de ataque deve liberar todas as linhas de cache na região compartilhada da memória referida peloshared_buffer. Como a região da memória é compartilhada, isso é simples e pode ser feito usando intrínsecos como_mm_clflush.Invoque
ReadBytecomuntrusted_indexa ser maior quebuffer_size. O contexto de ataque faz com que o contexto da vítima invoqueReadBytede tal forma que prevê incorretamente que o ramo não será tomado. Isso faz com que o processador execute especulativamente o corpo do bloco if comuntrusted_indexser maior quebuffer_size, levando assim a uma leitura fora dos limites debuffer. Consequentemente,shared_bufferé indexado usando um valor potencialmente secreto que foi lido fora dos limites, fazendo com que a respetiva linha de cache seja carregada pela CPU.Leia cada linha de cache em
shared_bufferpara ver qual é acessada mais rapidamente. O contexto de ataque pode ler cada linha de cache emshared_buffere detetar a linha de cache que carrega significativamente mais rápido do que as outras. Esta é a linha de cache que provavelmente tenha sido carregada pela etapa 3. Como há uma relação 1:1 entre o valor de byte e a linha de cache neste exemplo, isso permite que o invasor infera o valor real do byte que foi lido fora dos limites.
As etapas acima fornecem um exemplo de uso de uma técnica conhecida como FLUSH+RELOAD em conjunto com a exploração de uma instância de CVE-2017-5753.
Que cenários de software podem ser afetados?
O desenvolvimento de software seguro usando um processo como o Security Development Lifecycle (SDL) normalmente exige que os desenvolvedores identifiquem os limites de confiança existentes em seus aplicativos. Existe um limite de confiança em locais onde um aplicativo pode interagir com dados fornecidos por um contexto menos confiável, como outro processo no sistema ou um processo de modo de usuário não administrativo no caso de um driver de dispositivo de modo kernel. A nova classe de vulnerabilidades envolvendo canais laterais de execução especulativa é relevante para muitos dos perimetros de confiança nos modelos de segurança de software existentes que isolam código e dados num dispositivo.
A tabela a seguir fornece um resumo dos modelos de segurança de software em que os desenvolvedores podem precisar se preocupar com a ocorrência dessas vulnerabilidades:
| Limite de confiança | Descrição |
|---|---|
| Limite da máquina virtual | Aplicativos que isolam cargas de trabalho em máquinas virtuais separadas que recebem dados não confiáveis de outra máquina virtual podem estar em risco. |
| Limite do kernel | Um driver de dispositivo de modo kernel que recebe dados não confiáveis de um processo de modo de usuário não administrativo pode estar em risco. |
| Limite do processo | Um aplicativo que recebe dados não confiáveis de outro processo em execução no sistema local, como por meio de uma chamada de procedimento remoto (RPC), memória compartilhada ou outros mecanismos de comunicação Inter-Process (IPC) pode estar em risco. |
| Limite do enclave | Um aplicativo que é executado dentro de um enclave seguro (como o Intel SGX) que recebe dados não confiáveis de fora do enclave pode estar em risco. |
| Limite linguístico | Um aplicativo que interpreta ou Just-In-Time (JIT) compila e executa código não confiável escrito em uma linguagem de nível superior pode estar em risco. |
Os aplicativos que têm a superfície de ataque exposta a qualquer um dos limites de confiança acima devem revisar o código na superfície de ataque para identificar e mitigar possíveis instâncias de vulnerabilidades de canal lateral de execução especulativa. Deve-se notar que não foi demonstrado que os limites de confiança expostos a superfícies de ataque remoto, como protocolos de rede remotos, estão em risco de vulnerabilidades de canais laterais de execução especulativa.
Padrões de codificação potencialmente vulneráveis
Vulnerabilidades de canal lateral de execução especulativa podem surgir como consequência de vários padrões de codificação. Esta seção descreve padrões de codificação potencialmente vulneráveis e fornece exemplos para cada um, mas deve-se reconhecer que podem existir variações nesses temas. Como tal, os desenvolvedores são aconselhados a tomar esses padrões como exemplos e não como uma lista exaustiva de todos os padrões de codificação potencialmente vulneráveis. As mesmas classes de vulnerabilidades de segurança de memória que podem existir no software hoje também podem existir ao longo de caminhos especulativos e fora de ordem de execução, incluindo, mas não limitado a, saturações de buffer, acessos de matriz fora dos limites, uso de memória não inicializada, confusão de tipos e assim por diante. As mesmas primitivas que os invasores podem usar para explorar vulnerabilidades de segurança de memória ao longo de caminhos arquitetônicos também podem se aplicar a caminhos especulativos.
Em geral, os canais de execução especulativa relacionados com a falha na previsão de ramificações condicionais podem ocorrer quando uma expressão condicional opera em dados que podem ser controlados e influenciados por um contexto de menor confiança. Por exemplo, isso pode incluir expressões condicionais usadas em if, for, while, switch, ou declarações ternárias. Para cada uma dessas instruções, o compilador pode gerar uma ramificação condicional para a qual a CPU pode prever o destino da ramificação em tempo de execução.
Para cada exemplo, um comentário com a frase "BARREIRA DE ESPECULAÇÃO" é inserido onde um desenvolvedor poderia introduzir uma barreira como mitigação. Isso é discutido em mais detalhes na seção sobre mitigações.
Carga especulativa fora dos limites
Esta categoria de padrões de codificação envolve uma previsão incorreta de ramificação condicional que leva a um acesso especulativo à memória fora dos limites.
Carga fora dos limites do array alimentando uma carga
Este padrão de codificação é o padrão de codificação vulnerável originalmente descrito para CVE-2017-5753 (Bounds Check Bypass). A seção de plano de fundo deste artigo explica esse padrão em detalhes.
// A pointer to a shared memory region of size 1MB (256 * 4096)
unsigned char *shared_buffer;
unsigned char ReadByte(unsigned char *buffer, unsigned int buffer_size, unsigned int untrusted_index) {
if (untrusted_index < buffer_size) {
// SPECULATION BARRIER
unsigned char value = buffer[untrusted_index];
return shared_buffer[value * 4096];
}
}
Da mesma forma, uma carga fora dos limites da matriz pode ocorrer em conjunto com um loop que excede sua condição de terminação devido a uma previsão incorreta. Neste exemplo, o ramo condicional associado à expressão x < buffer_size pode prever incorretamente e executar especulativamente o corpo do loop for quando x é maior ou igual a buffer_size, resultando assim num carregamento especulativo fora dos limites.
// A pointer to a shared memory region of size 1MB (256 * 4096)
unsigned char *shared_buffer;
unsigned char ReadBytes(unsigned char *buffer, unsigned int buffer_size) {
for (unsigned int x = 0; x < buffer_size; x++) {
// SPECULATION BARRIER
unsigned char value = buffer[x];
return shared_buffer[value * 4096];
}
}
Carregamento fora dos limites de um array que alimenta uma ramificação indireta
Esse padrão de codificação envolve o caso em que uma previsão incorreta de ramificação condicional pode levar a um acesso fora dos limites a uma matriz de ponteiros de função que, em seguida, leva a uma ramificação indireta para o endereço de destino que foi lido fora dos limites. O trecho a seguir fornece um exemplo que demonstra isso.
Neste exemplo, um identificador de mensagem não confiável é fornecido a DispatchMessage por meio do untrusted_message_id parâmetro. Se untrusted_message_id for menor que MAX_MESSAGE_ID, então ele é usado para indexar em um array de ponteiros de função e desviar para o destino correspondente. Esse código é seguro arquitetonicamente, mas se a CPU previr incorretamente a ramificação condicional, ele pode resultar em DispatchTable ser indexado por untrusted_message_id quando seu valor é maior ou igual a MAX_MESSAGE_ID, levando a um acesso fora dos limites. Isso pode resultar em execução especulativa a partir de um endereço de destino de ramificação que é derivado além dos limites da matriz, o que pode levar à divulgação de informações, dependendo do código que é executado especulativamente.
#define MAX_MESSAGE_ID 16
typedef void (*MESSAGE_ROUTINE)(unsigned char *buffer, unsigned int buffer_size);
const MESSAGE_ROUTINE DispatchTable[MAX_MESSAGE_ID];
void DispatchMessage(unsigned int untrusted_message_id, unsigned char *buffer, unsigned int buffer_size) {
if (untrusted_message_id < MAX_MESSAGE_ID) {
// SPECULATION BARRIER
DispatchTable[untrusted_message_id](buffer, buffer_size);
}
}
Como no caso de uma carga fora dos limites da matriz alimentando outra carga, essa condição também pode surgir em conjunto com um loop que excede sua condição de terminação devido a uma previsão incorreta.
Array out-of-bounds armazena alimentando uma ramificação indireta
Embora o exemplo anterior tenha mostrado como uma carga especulativa fora dos limites pode influenciar um destino de ramificação indireta, também é possível que um armazenamento fora dos limites modifique um destino de ramificação indireta, como um ponteiro de função ou um endereço de retorno. Isso pode levar a execução especulativa a partir de um endereço especificado pelo atacante.
Neste exemplo, um índice não confiável é passado pelo untrusted_index parâmetro. Se untrusted_index for menor que a contagem de elementos da pointers (256), então o valor do ponteiro fornecido em ptr será gravado na matriz pointers. Este código é seguro em termos arquitetónicos, mas se a CPU fizer uma predição incorreta da ramificação condicional, isso pode resultar na escrita especulativa de ptr para além dos limites da matriz pointers alocada na pilha. Talvez resulte em corrupção especulativa do endereço de retorno para WriteSlot. Se um invasor possa controlar o valor de ptr, ele poderá causar execução especulativa a partir de um endereço arbitrário quando WriteSlot retorna ao longo do trajeto especulativo.
unsigned char WriteSlot(unsigned int untrusted_index, void *ptr) {
void *pointers[256];
if (untrusted_index < 256) {
// SPECULATION BARRIER
pointers[untrusted_index] = ptr;
}
}
Da mesma forma, se uma variável local de ponteiro de função nomeada func for alocada na pilha, então pode ser possível modificar de forma especulativa o endereço a que func se refere quando ocorre a previsão incorreta de ramificação condicional. Isso pode resultar em execução especulativa a partir de um endereço arbitrário quando o ponteiro da função é invocado.
unsigned char WriteSlot(unsigned int untrusted_index, void *ptr) {
void *pointers[256];
void (*func)() = &callback;
if (untrusted_index < 256) {
// SPECULATION BARRIER
pointers[untrusted_index] = ptr;
}
func();
}
Deve-se notar que ambos os exemplos envolvem modificação especulativa de ponteiros de ramificação indireta alocados em pilha. É possível que a modificação especulativa também possa ocorrer para variáveis globais, memória alocada no heap e até mesmo memória somente leitura em algumas CPUs. Para memória alocada em pilha, o compilador Microsoft C++ já executa etapas para dificultar a modificação especulativa de destinos de ramificação indireta alocados por pilha, como reordenar variáveis locais de modo que os buffers sejam colocados ao lado de um cookie de segurança como parte do recurso de segurança do /GS compilador.
Confusão de tipo especulativo
Esta categoria trata de padrões de codificação que podem dar origem a uma confusão de tipo especulativo. Isso ocorre quando a memória é acedida usando um tipo incorreto num caminho não arquitetural durante a execução especulativa. Tanto a previsão incorreta de ramo condicional quanto o desvio de armazenamento especulativo podem potencialmente levar a uma confusão de tipo especulativo.
Para desvio de armazenamento especulativo, isso pode ocorrer em cenários em que um compilador reutiliza um local de pilha para variáveis de vários tipos. Isso ocorre porque o armazenamento arquitetônico de uma variável do tipo A pode ser ignorado, permitindo assim que a carga do tipo A seja executada especulativamente antes que a variável seja atribuída. Se a variável armazenada anteriormente é de um tipo diferente, então isso pode criar as condições para uma confusão de tipo especulativo.
Para a previsão incorreta de ramo condicional, o trecho de código a seguir será usado para descrever diferentes condições que a confusão de tipo especulativo pode causar.
enum TypeName {
Type1,
Type2
};
class CBaseType {
public:
CBaseType(TypeName type) : type(type) {}
TypeName type;
};
class CType1 : public CBaseType {
public:
CType1() : CBaseType(Type1) {}
char field1[256];
unsigned char field2;
};
class CType2 : public CBaseType {
public:
CType2() : CBaseType(Type2) {}
void (*dispatch_routine)();
unsigned char field2;
};
// A pointer to a shared memory region of size 1MB (256 * 4096)
unsigned char *shared_buffer;
unsigned char ProcessType(CBaseType *obj)
{
if (obj->type == Type1) {
// SPECULATION BARRIER
CType1 *obj1 = static_cast<CType1 *>(obj);
unsigned char value = obj1->field2;
return shared_buffer[value * 4096];
}
else if (obj->type == Type2) {
// SPECULATION BARRIER
CType2 *obj2 = static_cast<CType2 *>(obj);
obj2->dispatch_routine();
return obj2->field2;
}
}
Confusão de tipo especulativo que leva a uma carga fora dos limites
Esse padrão de codificação envolve o caso em que uma confusão especulativa de tipos pode resultar num acesso a campo fora dos limites ou com tipos confusos, onde o valor carregado serve de base para um endereço de carregamento subsequente. Isso é semelhante ao padrão de codificação de matriz fora dos limites, mas é manifestado através de uma sequência de codificação alternativa, como mostrado acima. Neste exemplo, um contexto de ataque pode fazer com que o contexto da vítima seja executado ProcessType várias vezes com um objeto do tipo CType1 (type campo é igual a Type1). Isso terá o efeito de treinar o ramo condicional para a primeira if declaração a prever que não seja tomada. O contexto atacante pode então provocar que o contexto da vítima execute ProcessType com um objeto do tipo CType2. Isso pode resultar em uma confusão de tipo especulativo se o ramo condicional para a primeira if instrução prevê e executa erroneamente o corpo da if declaração, lançando assim um objeto do tipo CType2 para CType1. Como CType2 é menor do que CType1, o acesso a CType1::field2 resultará numa carga especulativa fora dos limites da memória de dados que podem ser secretos. Esse valor é então usado em uma carga a partir da shared_buffer qual pode criar efeitos colaterais observáveis, como com o exemplo de matriz fora dos limites descrito anteriormente.
Confusão de tipo especulativo conducente a um ramo indireto
Este padrão de codificação envolve o caso em que uma confusão de tipo especulativo pode resultar em um ramo indireto inseguro durante a execução especulativa. Neste exemplo, um contexto de ataque pode fazer com que o contexto da vítima seja executado ProcessType várias vezes com um objeto do tipo CType2 (type campo é igual a Type2). Isso terá o efeito de treinar o ramo condicional para que a primeira if declaração seja tomada e a else if declaração não seja tomada. O contexto atacante pode então provocar que o contexto da vítima execute ProcessType com um objeto do tipo CType1. Isso pode resultar em uma confusão de tipo especulativo se o ramo condicional para a primeira if instrução é prevista como tomada e a else if declaração é prevista como não tomada, executando assim o corpo do else if e convertendo um objeto do tipo CType1 para CType2. Uma vez que o campo CType2::dispatch_routine se sobrepõe ao array charCType1::field1, isso pode resultar numa ramificação indireta especulativa para um destino de ramificação não intencional. Se o contexto de ataque puder controlar os valores de byte na CType1::field1 matriz, eles poderão controlar o endereço de destino da ramificação.
Utilização especulativa não inicializada
Esta categoria de padrões de codificação envolve cenários onde a execução especulativa pode acessar a memória não inicializada e usá-la para alimentar uma carga subsequente ou ramificação indireta. Para que esses padrões de codificação sejam exploráveis, um invasor precisa ser capaz de controlar ou influenciar significativamente o conteúdo da memória usada sem ser inicializado pelo contexto em que está sendo usado.
Uso especulativo não inicializado levando a uma carga fora dos limites
Um uso especulativo não inicializado pode potencialmente levar a uma carga fora dos limites usando um valor controlado pelo invasor. No exemplo abaixo, o valor de index é atribuído a trusted_index em todas as vias arquitetônicas e trusted_index é assumido como menor ou igual a buffer_size. No entanto, dependendo do código produzido pelo compilador, é possível que ocorra um desvio de armazenamento especulativo que permite que a carga de buffer[index] e expressões dependentes sejam executadas antes da atribuição a index. Se isso ocorrer, um valor não inicializado para index será usado como deslocamento em buffer, o que poderia permitir que um invasor leia informações confidenciais fora da área permitida e as transmita por meio de um canal lateral através da carga dependente de shared_buffer.
// A pointer to a shared memory region of size 1MB (256 * 4096)
unsigned char *shared_buffer;
void InitializeIndex(unsigned int trusted_index, unsigned int *index) {
*index = trusted_index;
}
unsigned char ReadByte(unsigned char *buffer, unsigned int buffer_size, unsigned int trusted_index) {
unsigned int index;
InitializeIndex(trusted_index, &index); // not inlined
// SPECULATION BARRIER
unsigned char value = buffer[index];
return shared_buffer[value * 4096];
}
Uso especulativo não inicializado que leva a um ramo indireto
Um uso especulativo não inicializado pode potencialmente levar a uma ramificação indireta onde o alvo da ramificação é controlado por um invasor. No exemplo abaixo, routine é atribuído a DefaultMessageRoutine1 ou DefaultMessageRoutine dependendo do valor de mode. No caminho arquitetônico, isso resultará em routine ser sempre inicializado à frente do ramo indireto. No entanto, dependendo do código produzido pelo compilador, pode ocorrer uma omissão de armazenamento especulativa que permite que a ramificação indireta através de routine seja executada especulativamente antes da atribuição a routine. Se isso ocorrer, um invasor poderá executar especulativamente a partir de um endereço arbitrário, supondo que o invasor possa influenciar ou controlar o valor não inicializado de routine.
#define MAX_MESSAGE_ID 16
typedef void (*MESSAGE_ROUTINE)(unsigned char *buffer, unsigned int buffer_size);
const MESSAGE_ROUTINE DispatchTable[MAX_MESSAGE_ID];
extern unsigned int mode;
void InitializeRoutine(MESSAGE_ROUTINE *routine) {
if (mode == 1) {
*routine = &DefaultMessageRoutine1;
}
else {
*routine = &DefaultMessageRoutine;
}
}
void DispatchMessage(unsigned int untrusted_message_id, unsigned char *buffer, unsigned int buffer_size) {
MESSAGE_ROUTINE routine;
InitializeRoutine(&routine); // not inlined
// SPECULATION BARRIER
routine(buffer, buffer_size);
}
Opções de mitigação
As vulnerabilidades do canal lateral de execução especulativa podem ser atenuadas fazendo alterações no código-fonte. Essas alterações podem envolver a mitigação de instâncias específicas de uma vulnerabilidade, como a adição de uma barreira de especulação ou a realização de alterações no design de um aplicativo para tornar informações confidenciais inacessíveis à execução especulativa.
Barreira à especulação através de instrumentação manual
Uma barreira de especulação pode ser inserida manualmente por um desenvolvedor para evitar que a execução especulativa prossiga por um caminho não arquitetônico. Por exemplo, um desenvolvedor pode inserir uma barreira de especulação antes de um padrão de codificação perigoso no corpo de um bloco condicional, seja no início do bloco (após a ramificação condicional) ou antes da primeira carga que é motivo de preocupação. Isso impedirá que uma previsão incorreta de ramificação condicional execute o código perigoso em um caminho não arquitetônico serializando a execução. A sequência de barreira de especulação difere de acordo com a arquitetura de hardware, conforme descrito na tabela a seguir:
| Arquitetura | Barreira especulativa intrínseca para CVE-2017-5753 | Barreira especulativa intrínseca para CVE-2018-3639 |
|---|---|---|
| x86/x64 | _mm_lfence() | _mm_lfence() |
| BRAÇO | não disponível no momento | __dsb(0) |
| ARM64 | não disponível no momento | __dsb(0) |
Por exemplo, o padrão de código a seguir pode ser atenuado usando o intrínseco, _mm_lfence conforme mostrado abaixo.
// A pointer to a shared memory region of size 1MB (256 * 4096)
unsigned char *shared_buffer;
unsigned char ReadByte(unsigned char *buffer, unsigned int buffer_size, unsigned int untrusted_index) {
if (untrusted_index < buffer_size) {
_mm_lfence();
unsigned char value = buffer[untrusted_index];
return shared_buffer[value * 4096];
}
}
Barreira de especulação por instrumentação em tempo de compilação
O compilador Microsoft C++ no Visual Studio 2017 (a partir da versão 15.5.5) inclui suporte para a opção /Qspectre que automaticamente insere uma barreira de especulação para um conjunto limitado de padrões de codificação potencialmente vulneráveis relacionados ao CVE-2017-5753. A documentação para o /Qspectre sinalizador fornece mais informações sobre seus efeitos e uso. É importante notar que esse sinalizador não cobre todos os padrões de codificação potencialmente vulneráveis e, como tal, os desenvolvedores não devem confiar nele como uma mitigação abrangente para essa classe de vulnerabilidades.
Mascarar índices de matriz
Nos casos em que uma carga especulativa fora dos limites pode ocorrer, o índice de matriz pode ser fortemente limitado no caminho arquitetônico e não arquitetônico adicionando lógica para vincular explicitamente o índice de matriz. Por exemplo, se uma matriz pode ser alocada para um tamanho alinhado a uma potência de dois, então uma máscara simples pode ser introduzida. Isso é ilustrado no exemplo abaixo, onde se supõe que buffer_size está alinhado a uma potência de dois. Isso garante que untrusted_index seja sempre menor que buffer_size, mesmo que ocorra uma predição incorreta de um ramo condicional e untrusted_index tenha sido fornecido com um valor maior ou igual a buffer_size.
Deve-se notar que o mascaramento de índice realizado aqui pode estar sujeito a desvio especulativo de armazenamento, dependendo do código gerado pelo compilador.
// A pointer to a shared memory region of size 1MB (256 * 4096)
unsigned char *shared_buffer;
unsigned char ReadByte(unsigned char *buffer, unsigned int buffer_size, unsigned int untrusted_index) {
if (untrusted_index < buffer_size) {
untrusted_index &= (buffer_size - 1);
unsigned char value = buffer[untrusted_index];
return shared_buffer[value * 4096];
}
}
Removendo informações confidenciais da memória
Uma técnica que pode ser usada para mitigar vulnerabilidades de execução especulativa através de canais secundários é a remoção de informações confidenciais da memória. Os desenvolvedores de software podem procurar oportunidades para refatorar a sua aplicação de modo a garantir que informações confidenciais não sejam acessíveis durante a execução especulativa. Isso pode ser feito realizando a refatoração do design de uma aplicação para isolar informações confidenciais em processos separados. Por exemplo, um aplicativo de navegador da Web pode tentar isolar os dados associados a cada origem da Web em processos separados, impedindo assim que um processo seja capaz de acessar dados de origem cruzada por meio de execução especulativa.
Ver também
Diretrizes para a mitigação de vulnerabilidades de execução especulativa via canal colateral
Mitigação de vulnerabilidades de hardware em canais laterais de execução especulativa