Partilhar via


Manipulação de buffer

Um dos erros mais comuns dentro de qualquer driver está relacionado ao tratamento de buffer, onde 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 a manipulação de buffer e como evitá-los. Ele também identifica o código de exemplo WDK que demonstra técnicas adequadas de manipulação de buffer.

Tipos de buffer e endereços inválidos

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

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

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

Um endereço de memória inválido não é paginado nem não paginado. Como o sistema operacional trabalha para resolver uma falha de página causada pelo manuseio incorreto do buffer, ele leva 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).

  • Suscita o tipo de erro adequado. O sistema lida sempre com erros de buffer através de uma verificação de erro, como PAGE_FAULT_IN_NONPAGED_AREA, ou através de 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 de aplicação possa chamar que faça com que o driver proceda a uma checagem de erro é uma violação de segurança dentro do driver. Tal violação permite que um aplicativo cause ataques de negação de serviço a todo o sistema.

Pressupostos e erros comuns

Um dos problemas mais comuns nesta área é que os programadores de drivers assumem muito sobre o ambiente operacional. Alguns pressupostos e erros comuns incluem:

  • Um driver simplesmente verificando se o bit alto está definido no endereço. Confiar em 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, esta verificação não funciona em computadores com arquitetura x86 quando o sistema está a usar o ajuste de quatro gigabytes (4GT). Quando o 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 de modo de utilizador válido no momento da verificação. No entanto, não há garantias de que esse endereço permanecerá válido após a operação de sonda. Assim, esta técnica introduz uma condição de corrida sutil que pode levar a acidentes periódicos irreprodutíveis.

    As chamadas ProbeForRead e ProbeForWrite ainda são necessárias. Se um driver omitir a sonda, os utilizadores podem passar endereços válidos de modo kernel que um bloco de __try e __except (tratamento estruturado de exceções) não irá detetar, abrindo assim uma grande falha de segurança.

    A conclusão é que tanto a sondagem quanto o tratamento de exceções estruturadas são necessários:

    • A sondagem valida que o endereço é um endereço de modo de usuário e que 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 apenas valida se o endereço e o comprimento estão dentro do possível intervalo de endereços do modo de usuário (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. Como descrito para as funções da sonda, esta situação introduz uma condição de corrida que pode levar a colisões irreprodutíveis.

  • Um driver que não consegue usar o tratamento estruturado de exceções. 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 utilizar o tratamento de exceção estruturada em qualquer chamada que possa gerar uma exceção resultará num erro de verificação (normalmente KMODE_EXCEPTION_NOT_HANDLED).

    É um erro usar o tratamento de exceções estruturadas em torno de código que não se espera que gere erros. Este uso irá apenas mascarar bugs reais que de outra forma seriam encontrados. Colocar uma __try/__except envolvente no nível de despacho superior da sua rotina não é a solução correta para este problema, embora às vezes seja a solução reflexa tentada pelos desenvolvedores de drivers.

  • Um driver assumindo que o conteúdo da memória do usuário permanecerá estável. Por exemplo, suponha que um driver escreveu um valor em um local de memória de modo de usuário e, mais tarde, na mesma rotina, se referiu 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 de METHOD_NEITHER). Esses drivers manipulam diretamente os buffers do usuário e, portanto, devem incorporar métodos de precaução para o manuseio do buffer, a fim de 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 a E/S rápida for suportada.

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 atrá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 para envolver o código para tornar os buffers de utilizador zero. O código de exemplo em CdCommonRead parece usar as try palavras-chave e 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.