Compartilhar via


Manipulação de buffer

Um dos erros mais comuns em qualquer driver está relacionado ao tratamento de buffer, em que os buffers são inválidos ou muito pequenos. Esses erros podem permitir estouros de buffer ou causar falhas no sistema, o que pode comprometer a segurança do sistema. Este artigo discute alguns dos problemas comuns com o manuseio de buffer e como evitá-los. Ele também identifica o código de exemplo do WDK que demonstra técnicas adequadas de tratamento de buffer.

Tipos de buffer e endereços inválidos

Do ponto de vista do motorista, os buffers vêm em uma das duas variedades:

  • Buffers paginados, que podem ou não residir na memória.

  • Buffers não paginados, que devem residir na memória.

Um endereço de memória inválido não é paginado nem não paginado. À medida que o sistema operacional trabalha para resolver uma falha de página causada pelo manuseio incorreto do buffer, ele executa as seguintes etapas:

  • Ele isola o endereço inválido em um dos intervalos de endereços "padrão" (endereços de kernel paginados, endereços de kernel não paginados ou endereços de usuário).

  • Ele gera o tipo apropriado de erro. O sistema sempre lida com erros de buffer por uma verificação de bugs, como PAGE_FAULT_IN_NONPAGED_AREA, ou por uma exceção, como STATUS_ACCESS_VIOLATION. Se o erro for uma verificação de bug, o sistema interromperá a operação. No caso de uma exceção, o sistema invoca manipuladores de exceção baseados em pilha. Se nenhum dos manipuladores de exceção manipular a exceção, o sistema invocará uma verificação de bug.

Independentemente disso, qualquer caminho de acesso que um programa aplicativo possa chamar que faça com que o driver leve a uma verificação de bug é uma violação de segurança dentro do driver. Essa violação permite que um aplicativo cause ataques de negação de serviço a todo o sistema.

Suposições e erros comuns

Um dos problemas mais comuns nessa área é que os criadores de driver assumem muito sobre o ambiente operacional. Algumas suposições e erros comuns incluem:

  • Um driver simplesmente verificando se o bit alto está definido no endereço. Depender de um padrão de bits fixo para determinar o tipo de endereço não funciona em todos os sistemas ou cenários. Por exemplo, essa verificação não funciona em computadores baseados em x86 quando o sistema está usando o Ajuste de Quatro Gigabytes (4GT). Quando 4GT está sendo usado, os endereços do modo de usuário definem o bit alto para o terceiro gigabyte do espaço de endereço.

  • Um driver usando apenas ProbeForRead e ProbeForWrite para validar o endereço. Essas chamadas garantem que o endereço seja um endereço válido no modo de usuário no momento da investigação. No entanto, não há garantias de que esse endereço permanecerá válido após a operação de sondagem. Assim, essa técnica introduz uma condição de corrida sutil que pode levar a colisões periódicas irreproduzíveis.

    As chamadas ProbeForRead e ProbeForWrite ainda são necessárias. Se um driver omitir a investigação, os usuários poderão passar endereços válidos no modo kernel que um __try bloco e __except (tratamento de exceção estruturado) não capturará e, portanto, abrirá uma grande brecha de segurança.

    O resultado final é que tanto a sondagem quanto o tratamento estruturado de exceções são necessários:

    • A investigação valida se o endereço é um endereço no modo de usuário e se o comprimento do buffer está dentro do intervalo de endereços do usuário.

    • Um __try/__except bloco protege contra o acesso.

    Observe que ProbeForRead valida apenas se o endereço e o comprimento estão dentro do possível intervalo de endereços do modo de usuário (um pouco menos de 2 GB para um sistema sem 4GT, por exemplo), não se o endereço de memória é válido. Por outro lado, ProbeForWrite tenta acessar o primeiro byte em cada página do comprimento especificado para verificar se esses bytes são endereços de memória válidos.

  • Um driver que depende de funções do gerenciador de memória, como MmIsAddressValid , para garantir que o endereço seja válido. Conforme descrito para as funções de sonda, essa situação introduz uma condição de corrida que pode levar a falhas irreproduzíveis.

  • Um driver que falha ao usar o tratamento de exceção estruturado. As __try/except funções dentro do compilador usam suporte no nível do sistema operacional para tratamento de exceções. As exceções no nível do kernel são lançadas de volta ao sistema por meio de uma chamada para ExRaiseStatus ou uma das funções relacionadas. Um driver que não usa o tratamento de exceção estruturado em torno de qualquer chamada que possa gerar uma exceção levará a uma verificação de bug (normalmente KMODE_EXCEPTION_NOT_HANDLED).

    É um erro usar o tratamento de exceção estruturado em torno de um código que não deve gerar erros. Esse uso apenas mascarará bugs reais que, de outra forma, seriam encontrados. Colocar um __try/__except wrapper no nível de expedição superior de sua rotina não é a solução correta para esse problema, embora às vezes seja a solução reflexa tentada por escritores de driver.

  • Um driver que pressupõe que o conteúdo da memória do usuário permanecerá estável. Por exemplo, suponha que um driver tenha gravado um valor em um local de memória no modo de usuário e, posteriormente, na mesma rotina, se refira a esse local de memória. Um aplicativo mal-intencionado pode modificar ativamente essa memória após a gravação e, como resultado, fazer com que o driver falhe.

Para sistemas de arquivos, esses problemas são graves porque os sistemas de arquivos normalmente dependem do acesso direto aos buffers do usuário (o método de transferência METHOD_NEITHER). Esses drivers manipulam diretamente os buffers do usuário e, portanto, devem incorporar métodos de precaução para manipulação de buffer para evitar falhas no nível do sistema operacional. A E/S rápida sempre passa ponteiros de memória bruta, portanto, os drivers precisam se proteger contra problemas semelhantes se houver suporte para E/S rápida.

Código de exemplo para manipulação de buffer

O WDK contém vários exemplos de validação de buffer no código de exemplo do driver do sistema de arquivos fastfat e CDFS, incluindo:

  • A função FatLockUserBuffer em fastfat\deviosup.c usa MmProbeAndLockPages para bloquear as páginas físicas por trás do buffer do usuário e MmGetSystemAddressForMdlSafe em FatMapUserBuffer para criar um mapeamento virtual para as páginas que estão bloqueadas.

  • A função FatGetVolumeBitmap em fastfat\fsctl.c usa ProbeForRead e ProbeForWrite para validar buffers de usuário na API de desfragmentação.

  • A função CdCommonRead em cdfs\read.c usa __try e __except ao redor do código para zero buffers de usuário. O código de exemplo em CdCommonRead parece usar as try palavras-chave and except . No ambiente WDK, essas palavras-chave em C são definidas em termos das extensões __try do compilador e __except. Qualquer pessoa que use código C++ deve usar os tipos de compilador nativos para lidar com exceções corretamente, como __try é uma palavra-chave C++, mas não uma palavra-chave C, e fornecerá uma forma de tratamento de exceção C++ que não é válida para drivers de kernel.