Divulgação de memória do kernel no sistema operacional moderno

Debaixo do cortador, está a tradução da parte inicial do documento Detectando a divulgação da memória do kernel com emulação x86 e rastreamento de manchas ( Artigo Projeto Zero ), por Mateusz Jurczyk .


Na parte traduzida do documento:


  • Específicos da linguagem de programação C (como parte do problema de expansão de memória)
  • os detalhes da operação dos kernels Windows e Linux (como parte do problema de expansão de memória)
  • importância da divulgação da memória do kernel e impacto na segurança do SO
  • métodos e técnicas existentes para detectar e combater a divulgação da memória do kernel

Embora o documento enfoque os mecanismos de comunicação entre o kernel privilegiado do SO e os aplicativos do usuário, a essência do problema pode ser generalizada para qualquer transferência de dados entre diferentes domínios de segurança: o hipervisor é a máquina convidada, o serviço privilegiado do sistema (daemon) é o aplicativo da GUI, o cliente da rede é o servidor etc. .


KDPV


1. Introdução


Uma das tarefas dos sistemas operacionais modernos é garantir a separação de privilégios entre aplicativos do usuário e o kernel do sistema operacional. Em primeiro lugar, isso inclui o fato de que a influência de cada programa no tempo de execução deve ser limitada por uma determinada política de segurança e, em segundo lugar, que os programas podem acessar apenas as informações que têm permissão para ler. O segundo é difícil de fornecer, dadas as propriedades da linguagem C (a principal linguagem de programação usada no desenvolvimento do kernel), que torna extremamente difícil a transferência segura de dados entre diferentes domínios de segurança.


Os sistemas operacionais modernos que operam nas plataformas x86 / x86-64 são multithread e usam um modelo cliente-servidor no qual os aplicativos no modo usuário (clientes) são executados independentemente e chamam o kernel do SO (servidor) com a intenção de trabalhar com um recurso gerenciado pelo sistema. O mecanismo usado pelo código do modo de usuário ( anel 3 ) para chamar um conjunto predefinido de funções do kernel (anel 0) é chamado de chamadas do sistema ou (brevemente) syscalls. Uma chamada típica do sistema é mostrada na Figura 1:
Figura 1: Chamada do sistema
Figura 1: Ciclo de vida das chamadas do sistema.


É muito importante evitar o vazamento inadvertido do conteúdo da memória do kernel ao interagir com programas no modo de usuário. Existe um risco significativo de divulgar dados confidenciais do kernel. Os dados podem ser transmitidos implicitamente nos parâmetros de saída de chamadas do sistema seguras (de outros pontos de vista).


A divulgação da memória privilegiada do sistema ocorre quando o kernel do sistema operacional retorna uma região de memória maior (excesso) do que o necessário para armazenar as informações correspondentes (contidas dentro). Os bytes frequentemente redundantes contêm dados que foram preenchidos em um contexto diferente e, em seguida, a memória não foi pré-inicializada, o que impediria a disseminação de informações em novas estruturas de dados.


Específicos da linguagem de programação C


Nesta seção, examinamos vários aspectos da linguagem C que são mais importantes para o problema de expansão de memória.


Estado indefinido de variáveis ​​não inicializadas


Variáveis ​​individuais de tipos simples (como char ou int), bem como membros de estruturas de dados (matrizes, estruturas e uniões) permanecem em um estado indefinido até a primeira inicialização (independentemente de serem colocadas na pilha ou na pilha). Citações relevantes da especificação C11 (ISO / IEC 9899: 201x Comitê N1570, abril de 2011):


6.7.9 Inicialização
...
10 Se um objeto com duração de armazenamento automático não for inicializado explicitamente, seu valor será indeterminado .

7.22.3.4 A função malloc
...
2 A função malloc aloca espaço para um objeto cujo tamanho é especificado por tamanho e cujo valor é indeterminado .

7.22.3.5 A função realloc
...
2 A função realloc desaloca o objeto antigo apontado por ptr e retorna um ponteiro para um novo objeto que possui o tamanho especificado por tamanho. O conteúdo do novo objeto deve ser o mesmo que o do objeto antigo antes da desalocação, até o menor dos tamanhos novo e antigo. Quaisquer bytes no novo objeto além do tamanho do objeto antigo têm valores indeterminados .

A parte que se aplica ao código do sistema é mais relevante para os objetos localizados na pilha, pois o kernel do SO geralmente possui interfaces de alocação dinâmica com sua própria semântica (não necessariamente compatível com a biblioteca C padrão, como será descrito mais adiante).


Até onde sabemos, nenhum dos três compiladores C mais populares para Windows e Linux (Microsoft C / C ++ Compiler, gcc, LLVM) cria código que pré-inicializa variáveis ​​não inicializadas por programadores na pilha no modo Release-build (ou equivalente). Existem opções do compilador para marcar quadros de pilha com bytes especiais - marcadores (/ RTCs no Microsoft Visual Studio, por exemplo), mas eles não são usados ​​nas versões do Release por motivos de desempenho. Como resultado, variáveis ​​não inicializadas na pilha herdam os valores antigos das áreas de memória correspondentes.


Considere um exemplo de implementação padrão de uma chamada de sistema fictícia do Windows que multiplica um número inteiro de entrada por dois e retorna o resultado da multiplicação (Listagem 1). Obviamente, no caso especial (InputValue == 0), a variável OutputValue permanece não inicializada e é copiada de volta para o cliente. Este erro permite abrir quatro bytes de memória da pilha do kernel para cada chamada.


NTSTATUS NTAPI NtMultiplyByTwo(DWORD InputValue, LPDWORD OutputPointer) { DWORD OutputValue; if (InputValue != 0) { OutputValue = InputValue * 2; } *OutputPointer = OutputValue; return STATUS_SUCCESS; } 

Listagem de código 1: Expandindo a memória por meio de uma variável local não inicializada.


Os vazamentos através de uma variável local não inicializada não são muito comuns na prática: por um lado, os compiladores modernos frequentemente detectam e alertam sobre esses problemas; por outro lado, esses vazamentos são erros funcionais que podem ser detectados durante o desenvolvimento ou o teste. No entanto, o segundo exemplo (na Listagem 2) mostra que um vazamento também pode ocorrer através do campo de estrutura.


Nesse caso, o campo de estrutura reservada nunca é explicitamente usado no código, mas ainda é copiado para o modo de usuário e, portanto, também expõe quatro bytes de memória do kernel ao código de chamada. Este exemplo mostra claramente que inicializar cada campo de cada estrutura retornada ao cliente para todas as ramificações da execução de código não é uma tarefa fácil. Em muitos casos, a inicialização forçada parece ilógica, especialmente se esse campo não desempenhar nenhum papel prático. Mas é o fato de que uma variável não inicializada (ou campo de estrutura) na pilha (ou na pilha) aceita o conteúdo dos dados armazenados anteriormente nessa área de memória (no contexto de outra operação), está no coração do problema de expansão de memória do kernel.


 typedef struct _SYSCALL_OUTPUT { DWORD Sum; DWORD Product; DWORD Reserved; } SYSCALL_OUTPUT, *PSYSCALL_OUTPUT; NTSTATUS NTAPI NtArithOperations( DWORD InputValue, PSYSCALL_OUTPUT OutputPointer ) { SYSCALL_OUTPUT OutputStruct; OutputStruct.Sum = InputValue + 2; OutputStruct.Product = InputValue * 2; RtlCopyMemory(OutputPointer, &OutputStruct, sizeof(SYSCALL_OUTPUT)); return STATUS_SUCCESS; } 

Listagem 2: Expandindo a memória através de um campo de estrutura reservado.


Alinhamento de estruturas e bytes de preenchimento


A inicialização de todos os campos da estrutura de saída é um bom começo para evitar a expansão da memória. Mas isso não é suficiente para garantir que na representação de baixo nível não haja bytes não inicializados. Vamos voltar à especificação C11:


6.5.3.4 Os operadores sizeof e Alignof
...
4 [...] Quando aplicado a um operando que possui estrutura ou tipo de união, o resultado é o número total de bytes desse objeto, incluindo preenchimento interno e à direita .

6.2.8 Alinhamento de objetos
1 Os tipos de objetos completos têm requisitos de alinhamento que impõem restrições aos endereços nos quais os objetos desse tipo podem ser alocados . Um alinhamento é um valor inteiro integrado definido pela implementação que representa o número de bytes entre endereços sucessivos nos quais um determinado objeto pode ser alocado. [...]

6.7.2.1 Especi fi cadores de estrutura e união
...
17 Pode haver preenchimento sem nome no final de uma estrutura ou união .

Ou seja, os compiladores da linguagem C para arquiteturas x86 (-64) usam o alinhamento natural dos campos de estruturas (com um tipo primitivo): cada um desses campos é alinhado por N bytes, onde N é o tamanho do campo. Além disso, estruturas e junções inteiras também são alinhadas quando declaradas em uma matriz, e o requisito para alinhamento de campos aninhados é atendido. Para garantir o alinhamento, bytes de preenchimento implícito são inseridos nas estruturas, quando necessário. Embora não estejam diretamente acessíveis no código-fonte, esses bytes também herdam valores antigos das áreas de memória e podem transmitir informações para o modo de usuário.


No exemplo da Listagem 3, a estrutura SYSCALL_OUTPUT é retornada de volta ao código de chamada. Ele contém campos de 4 e 8 bytes, separados por 4 bytes de preenchimento, necessários para que o endereço do campo LargeSum se torne um múltiplo de 8. Apesar de ambos os campos terem sido inicializados corretamente, os bytes de preenchimento não são definidos explicitamente, o que novamente leva à expansão da memória da pilha do kernel. A localização específica da estrutura na memória é mostrada na Figura 2.


 typedef struct _SYSCALL_OUTPUT { DWORD Sum; QWORD LargeSum; } SYSCALL_OUTPUT, *PSYSCALL_OUTPUT; NTSTATUS NTAPI NtSmallSum( DWORD InputValue, PSYSCALL_OUTPUT OutputPointer ) { SYSCALL_OUTPUT OutputStruct; OutputStruct.Sum = InputValue + 2; OutputStruct.LargeSum = 0; RtlCopyMemory(OutputPointer, &OutputStruct, sizeof(SYSCALL_OUTPUT)); return STATUS_SUCCESS; } 

Listagem 3: Expandindo a memória alinhando a estrutura.


Figura 2: Alinhando a estrutura
Figura 2: Representação da estrutura na memória com o alinhamento em mente.


Vazamentos através de alinhamentos são relativamente comuns, pois muitos parâmetros de saída de chamadas do sistema são representados por estruturas. O problema é especialmente grave para plataformas de 64 bits, onde o tamanho dos ponteiros, size_t e tipos semelhantes aumenta de 4 para 8 bytes, o que leva ao aparecimento de preenchimento necessário para alinhar os campos dessas estruturas.


Como os bytes de preenchimento não podem ser endereçados no código-fonte, é necessário usar o memset ou uma função semelhante para redefinir toda a área de memória da estrutura antes de inicializar qualquer um de seus campos e copiá-lo para o modo de usuário, por exemplo:


  memset(&OutputStruct, 0, sizeof(OutputStruct)); 

No entanto, o Seacord RC em seu livro "The CERT C Coding Standard, Segunda Edição: 98 Regras para o Desenvolvimento de Sistemas Seguros, Confiáveis ​​e Seguros. Addison-Wesley Professional" 2014 afirma que essa não é uma solução ideal porque o preenchimento de bytes ) ainda pode ser desativado depois de chamar memset, por exemplo, como um efeito colateral de operações com campos adjacentes. A preocupação pode ser justificada pela seguinte declaração na especificação C:


6.2.6 Representações de tipos
6.2.6.1 Geral
...
6 Quando um valor é armazenado em um objeto de estrutura ou tipo de união , inclusive em um objeto membro, os bytes da representação do objeto que correspondem a quaisquer bytes de preenchimento recebem valores não especificados . [...]

No entanto, na prática, nenhum dos compiladores C testados lemos ou gravamos fora das áreas de memória dos campos declarados explicitamente. Parece que essa opinião é compartilhada pelos desenvolvedores de sistemas operacionais que usam memset.


Uniões e campos de tamanhos diferentes


As junções são outra construção complexa da linguagem C no contexto da comunicação com código de chamada menos privilegiado. Considere como a especificação C11 descreve a representação de uniões na memória:


6.2.5 Tipos
...
20 Qualquer número de tipos derivados pode ser construído a partir dos tipos de objeto e função, como segue: [...] Um tipo de união descreve um conjunto não vazio de objetos membros sobrepostos , cada um dos quais com um nome opcionalmente especificado e um tipo possivelmente distinto.

6.7.2.1 Especi fi cadores de estrutura e união
...
6 Conforme discutido em 6.2.5, uma estrutura é um tipo que consiste em uma sequência de membros, cujo armazenamento é alocado em uma sequência ordenada, e uma união é um tipo que consiste em uma sequência de membros cujo armazenamento se sobrepõe .
...
16 O tamanho de um sindicato é suficiente para conter o maior de seus membros . O valor de no máximo um dos membros pode ser armazenado em um objeto de união a qualquer momento.

O problema é que, se a união consistir em vários campos de tamanhos diferentes e apenas um campo de tamanho menor for explicitamente inicializado, os bytes restantes alocados para acomodar campos grandes permanecerão não inicializados. Vejamos um exemplo de um manipulador de chamadas do sistema hipotético, mostrado na Listagem 4, junto com a alocação de memória de união SYSCALL_OUTPUT mostrada na Figura 3.


 typedef union _SYSCALL_OUTPUT { DWORD Sum; QWORD LargeSum; } SYSCALL_OUTPUT, *PSYSCALL_OUTPUT; NTSTATUS NTAPI NtSmallSum( DWORD InputValue, PSYSCALL_OUTPUT OutputPointer ) { SYSCALL_OUTPUT OutputStruct; OutputStruct.Sum = InputValue + 2; RtlCopyMemory(OutputPointer, &OutputStruct, sizeof(SYSCALL_OUTPUT)); return STATUS_SUCCESS; } 

Listagem de código 4: Expandindo a memória inicializando parcialmente uma união.


Figura 3: Alinhando uma junção
Figura 3: Representação da união na memória com alinhamento.


Acontece que o tamanho total da união SYSCALL_OUTPUT é de 8 bytes (devido ao tamanho do campo LargeSum maior). No entanto, a função define apenas o valor do campo menor, deixando 4 bytes à direita não inicializados, o que posteriormente leva a um vazamento no aplicativo cliente.


Uma implementação segura deve definir apenas o campo Soma no espaço de endereço do usuário e não copiar o objeto inteiro com áreas de memória potencialmente não utilizadas. Outra correção de trabalho é chamar a função memset para anular uma cópia da união na memória do kernel antes de definir qualquer um de seus campos e transferi-la de volta ao modo de usuário.


Tamanho inseguro


Conforme mostrado nas duas seções anteriores, o uso do operador sizeof pode contribuir direta ou indiretamente para revelar a memória do kernel, fazendo com que mais dados sejam copiados do que o inicializado anteriormente.


C não possui o aparato necessário para transferir com segurança os dados do kernel para o espaço do usuário - ou, mais geralmente, entre contextos de segurança diferentes. O idioma não contém metadados de tempo de execução que podem indicar explicitamente quais bytes foram definidos em cada estrutura de dados usada para interagir com o kernel do SO. Como resultado, a responsabilidade recai sobre o programador, que deve determinar quais partes de cada objeto devem ser passadas para o código de chamada. Se feito corretamente, você precisará escrever uma função de cópia segura separada para cada estrutura de saída usada nas chamadas do sistema. O que, por sua vez, levará a um inchaço no tamanho do código, a uma deterioração de sua legibilidade e, em geral, será uma tarefa tediosa e demorada.


Por outro lado, é conveniente e simples copiar toda a área de memória do kernel com uma única chamada de memória e o argumento sizeof, além de permitir que o cliente determine quais partes da saída serão usadas. Acontece que essa abordagem é usada hoje em Windows e Linux. E quando um caso específico de vazamento de informações é detectado, um patch com uma chamada memset é imediatamente fornecido e distribuído pelo fabricante do SO. Infelizmente, isso não resolve o problema no caso geral.


Específicos do SO


Existem certas soluções de design de kernel, métodos de programação e padrões de código que afetam a tendência do sistema operacional a vulnerabilidades de expansão de memória. Eles são considerados nas seguintes subseções.


Reutilizando a memória dinâmica


Os alocadores atuais de memória dinâmica (no modo de usuário e no kernel) são altamente otimizados, pois seu desempenho tem um impacto significativo no desempenho de todo o sistema. Uma das otimizações mais importantes é a reutilização da memória: quando liberada, a memória correspondente raramente é completamente descartada; em vez disso, é salva na lista de regiões prontas para serem retornadas na próxima vez em que for alocada. Para salvar os ciclos da CPU, as áreas de memória padrão não são limpas entre a desalocação e a nova alocação. Como resultado disso, duas partes desconectadas do kernel trabalham com o mesmo intervalo de memória por um curto período de tempo. Isso significa que o vazamento do conteúdo da memória dinâmica do kernel permite revelar os dados de vários componentes do sistema operacional.


Nos parágrafos seguintes, apresentamos uma breve visão geral dos alocadores usados ​​nos kernels do Windows e Linux e suas qualidades mais notáveis.


Windows
A função principal do gerenciador de pool de kernel do Windows é ExAllocatePoolWithTag , que pode ser chamada diretamente ou através de um dos shells disponíveis: ExAllocatePool {∅, Ex, WithQuotaTag, WithTagPriority}. Nenhuma dessas funções libera o conteúdo da memória retornada, por padrão ou através de qualquer sinalizador de entrada. Pelo contrário, todos eles têm o seguinte aviso na respectiva documentação do MSDN:


Nota A memória que a função aloca não é inicializada. Um driver no modo kernel deve zerar essa memória primeiro para torná-la visível para o software no modo usuário (para evitar vazamentos de conteúdo potencialmente privilegiado).

O código de chamada pode selecionar um dos seis principais tipos de conjuntos: NonPagedPool, NonPagedPoolNx, NonPagedPoolSession, NonPagedPoolSessionNx, PagedPool e PagedPoolSession. Cada um deles possui uma região separada no espaço de endereço virtual e, portanto, as áreas de memória alocadas podem ser reutilizadas apenas no mesmo tipo de pool. A frequência de reutilização de pedaços de memória é muito alta e as áreas zeradas geralmente são retornadas apenas se um registro adequado não for encontrado nas listas de lookaside ou se a solicitação for tão grande que novas páginas de memória sejam necessárias. Em outras palavras, atualmente não há praticamente nenhum fator que impeça a divulgação da memória do pool no Windows, e quase todos esses erros podem ser usados ​​para vazar dados confidenciais de diferentes partes do kernel.


Linux
O kernel do Linux possui três interfaces principais para alocar memória dinamicamente:


  • kmalloc - uma função comum usada para alocar blocos de memória de tamanho arbitrário (contínuo no espaço de endereço virtual e físico), usa alocação de memória de laje .
  • kmem_cache_create e kmem_cache_alloc - um mecanismo especializado para alocar objetos de tamanho fixo (estruturas, por exemplo), também usa alocação de memória de laje .
  • O vmalloc é uma função de alocação raramente usada que retorna regiões cuja continuidade não é garantida no nível da memória física.

Essas funções (por si só) não garantem que as regiões selecionadas não contenham dados antigos (potencialmente confidenciais), o que possibilita abrir a memória do heap do kernel. No entanto, existem várias maneiras pelas quais o código de chamada pode solicitar memória nula:


  • A função kmalloc possui um análogo do kzalloc , o que garante que a memória retornada seja limpa.
  • O sinalizador opcional __GFP_ZERO pode ser passado para kmalloc , kmem_cache_alloc e algumas outras funções para obter o mesmo resultado.
  • kmem_cache_create aceita um ponteiro para uma função construtora opcional que é chamada para pré-inicializar cada objeto antes de retorná-lo ao código de chamada. O construtor pode ser implementado como um wrapper em torno de um memset para zerar uma determinada área de memória.

Vemos a disponibilidade dessas opções como condições favoráveis ​​à segurança do kernel, pois incentivam os desenvolvedores a tomar decisões informadas e permitem que eles simplesmente trabalhem com as funções de alocação de memória existentes, em vez de adicionar chamadas memset adicionais após cada alocação de memória dinâmica.


Matrizes de tamanho fixo


O acesso a vários recursos do sistema operacional pode ser obtido por seus nomes de teste. A variedade de recursos nomeados no Windows é muito grande, por exemplo: arquivos e diretórios, chaves e valores de chaves do registro, janelas, fontes e muito mais. Para alguns deles, o comprimento do nome é limitado e é expresso por uma constante, como MAX_PATH (260) ou LF_FACESIZE (32). Nesses casos, os desenvolvedores do kernel geralmente simplificam o código declarando os buffers de tamanho máximo e copiando-os como um todo (por exemplo, usando a palavra-chave sizeof) em vez de trabalhar apenas com a parte correspondente da linha. Isso é especialmente útil se as strings forem membros de estruturas maiores. Esses objetos podem ser movidos livremente na memória sem se preocupar em gerenciar ponteiros para a memória dinâmica.


Como seria de esperar, buffers grandes raramente são usados ​​completamente e o espaço de armazenamento restante geralmente não é liberado. Isso pode levar a vazamentos particularmente graves de longas áreas contíguas da memória do kernel. No exemplo da Listagem 5, a chamada do sistema usa a função RtlGetSystemPath para carregar o caminho do sistema no buffer local e, se a chamada for bem-sucedida, todos os 260 bytes serão passados ​​para o chamador, independentemente do comprimento real da linha.


 NTSTATUS NTAPI NtGetSystemPath(PCHAR OutputPath) { CHAR SystemPath[MAX_PATH]; NTSTATUS Status; Status = RtlGetSystemPath(SystemPath, sizeof(SystemPath)); if (NT_SUCCESS(Status)) { RtlCopyMemory(OutputPath, SystemPath, sizeof(SystemPath)); } return Status; } 

Listagem 5: Expandindo a memória inicializando parcialmente o buffer de cadeia.


A região da memória copiada de volta para o espaço do usuário neste exemplo é mostrada na Figura 4.


Figura 4: Memória de buffer de seqüência inicializada
Figura 4: Memória de um buffer de linha parcialmente inicializado.


Uma implementação segura deve retornar apenas o caminho solicitado, e não o buffer inteiro usado para armazenamento. Este exemplo mais uma vez demonstra como a estimativa do tamanho dos dados com o operador sizeof (usado como parâmetro para RtlCopyMemory) pode estar completamente incorreta com relação à quantidade real de dados que o kernel deve passar para a área do usuário.


Tamanho da saída da chamada do sistema arbitrário


A maioria das chamadas do sistema aceita ponteiros para a saída do modo de usuário, juntamente com o tamanho do buffer. Na maioria dos casos, as informações de tamanho devem ser usadas apenas para determinar se o buffer fornecido é suficiente para receber a saída de chamada do sistema. Não use o tamanho inteiro do buffer de saída fornecido para especificar a quantidade de memória a ser copiada. No entanto, vemos casos em que o kernel tenta usar todos os bytes do buffer de saída do usuário, sem contar a quantidade de dados reais que precisam ser copiados. Um exemplo desse comportamento é mostrado na Listagem 6.


 NTSTATUS NTAPI NtMagicValues(LPDWORD OutputPointer, DWORD OutputLength) { if (OutputLength < 3 * sizeof(DWORD)) { return STATUS_BUFFER_TOO_SMALL; } LPDWORD KernelBuffer = Allocate(OutputLength); KernelBuffer[0] = 0xdeadbeef; KernelBuffer[1] = 0xbadc0ffe; KernelBuffer[2] = 0xcafed00d; RtlCopyMemory(OutputPointer, KernelBuffer, OutputLength); Free(KernelBuffer); return STATUS_SUCCESS; } 

Listagem 6: Expandindo a memória por meio de um buffer de saída de tamanho arbitrário.


O objetivo de uma chamada do sistema é fornecer ao código de chamada três valores especiais de 32 bits, ocupando um total de 12 bytes. Embora a verificação do tamanho correto do buffer no início da função esteja correta, o uso do argumento OutputLength deve terminar aí. Sabendo que o buffer de saída é grande o suficiente para salvar o resultado, o kernel pode alocar 12 bytes de memória, preenchê-lo e copiar o conteúdo de volta para o buffer fornecido no modo de usuário. Em vez disso, uma chamada do sistema aloca um bloco de pool (além disso, com um comprimento controlado pelo usuário) e copia toda a memória alocada no espaço do usuário. Acontece que todos os bytes, exceto os 12 primeiros, não são inicializados e são abertos erroneamente ao usuário, como mostra a Figura 5.


Figura 5: Memória de buffer arbitrária
Figura 5: Memória buffer de tamanho arbitrário.


O esquema discutido nesta seção é especialmente comum para o Windows. Um erro semelhante pode fornecer ao invasor uma primitiva extremamente útil para expansão de memória:


  • , Windows, . , .
  • . , , . , ( — ) .

, . , , .


,


, . , Windows .



, , . , : AddressSanitizer , PageHeap Special Pool . , , - . , . , , , , , . , ( ).


, , , . , .


, API
API, Windows (Win32/User32 API). API , , , . , , , , . .



, . , . , , , . , , .


, , . , KASLR (Kernel Address Space Layout Randomization ), . : Windows, Hacking Team 2015 ( Juan Vazquez. Revisiting an Info Leak ) (derandomize) win32k.sys, . , Matt Tait' Google Project Zero ( Kernel-mode ASLR leak via uninitialized memory returned to usermode by NtGdiGetTextMetrics ) MS15-080 (CVE-2015-2433).



(/) , , (control flow), : , , , , StackGuard Linux /GS Windows . , . , , .


(/)
(/) , , , : , , , . , , . . , ( , ) , , .



KDPV # 2


Microsoft Windows



2015 Windows. 2015 Matt Tait win32k!NtGdiGetTextMetrics. Windows Hacking Team. , , , 0-day Windows.


2015, WanderingGlitch (HP Zero Day Initiative) ( Acknowledgments – 2015 ). Ruxcon 2016 ( ) "Leaking Windows Kernel Pointers" .


, 2017 fanxiaocao pjf IceSword Lab (Qihoo 360) "Automatically Discovering Windows Kernel Information Leak Vulnerabilities" , , 14 2017 (8 ). Bochspwn Reloaded, , . VMware (Bochs) . , Bochspwn Reloaded, .


, , 2010-2011 , win32k: "Challenge: On 32bit Windows7, explain where the upper 16bits of eax come from after a call to NtUserRegisterClassExWOW()" "Subtle information disclosure in WIN32K.SYS syscall return values" . Windows 8, 2015 Matt Tait , : Google Project Zero Bug Tracker .



( ), , 2017 - Windows -, : Joseph Bialek — "Anyone notice my change to the Windows IO Manager to generically kill a class of info disclosure? BufferedIO output buffer is always zero'd" . , IOCTL- .


, Visual Studio 15.5 POD- , "= {0}", . , padding- () .


Linux


Windows, Linux , 2010 . , ( ) ( ) . , Windows Linux , — , .



, Linux . "Linux kernel vulnerabilities: State-of-the-art defenses and open problems" 2010 2011 28 . 2017- "Securing software systems by preventing information leaks" Lu K. 59 , 2013- 2016-. . : Rosenberg Oberheide 25 , Linux 2009-2010 , . Linux c grsecurity / PaX-hardened . Vasiliy Kulikov 25 2010-2011 , Coccinelle . , Mathias Krause 21 2013 50 .


, , Linux. — -Wuninitialized ( gcc, LLVM), . kmemcheck , Valgrind' . , . , KernelAddressSANitizer KernelMemorySANitizer . KMSAN syzkaller ( ) 19 , .


Linux. 2014 — 2016 Peir´o Coccinelle , Linux 3.12: "Detecting stack based kernel information leaks" International Joint Conference SOCO14-CISIS14-ICEUTE14, pages 321–331 (Springer, 2014) "An analysis on the impact and detection of kernel stack infoleaks" Logic Journal of the IGPL. , . 2016- Lu UniSan — , , : , . , 20% (350 1800), 19 Linux Android.


— (multi-variant program execution), , . , . , KASLR, -, . , 2006 DieHard: probabilistic memory safety for unsafe languages, 2017 — BUDDY: Securing software systems by preventing information leaks. John North "Identifying Memory Address Disclosures" 2015- . , SafeInit (Comprehensive and Practical Mitigation of Uninitialized Read Vulnerabilities) , , . , , , Linux.



, . , : , . , , - , . .


CONFIG_PAGE_POISONING CONFIG_DEBUG_SLAB, -. -, . , , , Linux.


grsecurity / PaX . , PAX_MEMORY_SANITIZE , slab , ( — ). , PAX_MEMORY_STRUCTLEAK , ( ), . padding- (), 100% . , — PAX_MEMORY_STACKLEAK, . , , . (Kernel Self Protection Project) STACKLEAK .


Linux:


Secure deallocation, Chow , 2005

Chow, Jim and Pfaff, Ben and Garfinkel, Tal and Rosenblum, Mendel. Shredding Your Garbage: Reducing Data Lifetime Through Secure Deallocation. In USENIX Security Symposium, pages 22–22, 2005.


, , ( ) . Linux .


Split Kernel, Kurmus Zippel, 2014

Kurmus, Anil and Zippel, Robby. A tale of two kernels: Towards ending kernel hardening wars with split kernel. In Proceedings of the 2014 ACM SIGSAC Conference on Computer and Communications Security, pages 1366–1377. ACM, 2014.


, .


SafeInit, Milburn , 2017

Milburn, Alyssa and Bos, Herbert and Giuffrida, Cristiano. SafeInit: Comprehensive and Practical Mitigation of Uninitialized Read Vulnerabilities. In Proceedings of the 2017 Annual Network and Distributed System Security Symposium (NDSS)(San Diego, CA), 2017.


, , .


UniSan, Lu , 2016

Lu, Kangjie and Song, Chengyu and Kim, Taesoo and Lee, Wenke. UniSan: Proactive kernel memory initialization to eliminate data leakages. In Proceedings of the 2016 ACM SIGSAC Conference on Computer and Communications Security, pages 920–932. ACM, 2016.


SafeInit , , , , .


, Linux .


( )


, , ( ). : (), , , , ( - ) . , . , , .


, :


  • Bochspwn Reloaded – detection with software x86 emulation
  • Windows bug reproduction techniques
  • Alternative detection methods
  • Other data sinks
  • Future work
  • Other system instrumentation schemes

, :) , .

Source: https://habr.com/ru/post/pt415685/


All Articles