Criando uma DLL de proxy para verificações de operação de seqüestro de DLL

Quando examino a segurança do software, um dos pontos a verificar é trabalhar com bibliotecas dinâmicas. Ataques como o DLL de seqüestro ("dll spoofing" ou "dll interception") são muito raros. Provavelmente, isso se deve ao fato de que os desenvolvedores do Windows adicionam mecanismos de segurança para evitar ataques, e os desenvolvedores de software são mais cuidadosos com a segurança. Mas o mais interessante é a situação em que o software de destino está vulnerável.

Descrevendo brevemente o ataque, a DLL de seqüestro está criando uma situação na qual algum executável tenta carregar a dll, mas o invasor intervém nesse processo e, em vez da biblioteca esperada, uma dll especialmente preparada é carregada com a carga útil do invasor. Como resultado, o código da dll será executado com os direitos do aplicativo iniciado; portanto, os aplicativos com direitos mais altos geralmente são selecionados como destino.

Para que a biblioteca seja carregada corretamente, várias condições devem ser atendidas: o tamanho do bit do arquivo executável e a biblioteca devem corresponder e, se a biblioteca for carregada quando o aplicativo for iniciado, a dll deverá exportar todas as funções que esse aplicativo espera importar. Frequentemente, uma importação não é suficiente - é muito desejável que o aplicativo continue seu trabalho após carregar a dll. Para isso, é necessário que as funções da biblioteca preparada funcionem da mesma forma que o original. A maneira mais fácil de fazer isso é simplesmente passando as chamadas de função de uma biblioteca para outra. Essas são as DLLs chamadas proxy dlls.



Sob o corte, haverá várias opções para criar essas bibliotecas - na forma de código e utilitários.

Uma pequena revisão teórica


As bibliotecas são carregadas com mais frequência usando a função LoadLibrary, na qual o nome da biblioteca é passado. Se, em vez do nome, você passar o caminho completo, o aplicativo tentará carregar a biblioteca especificada. Por exemplo, chamar LoadLibrary ("C: \ Windows \ system32 \ version.dll") carregará a dll especificada. Ou, se a biblioteca não existir, ela não será carregada.

Um pouco de tédio
Se alguma dll já estiver carregada no aplicativo, ela não será carregada novamente. Como o version.dll é carregado no início de quase qualquer arquivo exe, na verdade, a chamada acima não carrega nada. Mas ainda consideramos o caso geral, considere o exemplo como uma chamada para alguma biblioteca abstrata.

É outra questão se você escrever LoadLibrary ("version.dll"). Em uma situação normal, o resultado será exatamente o mesmo do caso anterior - C: \ Windows \ system32 \ version.dll será carregado, mas não tão simples.

Primeiro, será pesquisada uma biblioteca, que será na seguinte ordem :

  1. Pasta executável
  2. Pasta C: \ Windows \ System32
  3. Pasta C: \ Windows \ System
  4. Pasta C: \ Windows
  5. A pasta definida como atual para o aplicativo
  6. Pastas da variável de ambiente PATH

Um pouco mais de tédio
Ao iniciar aplicativos de 32 bits em um sistema de 64 bits, todas as chamadas para C: \ Windows \ system32 serão encaminhadas para C: \ Windows \ SysWOW64. Isso é apenas pela precisão da descrição, do ponto de vista do atacante a diferença não é particularmente importante.

Quando você executa o arquivo exe, o sistema operacional carrega todas as bibliotecas da seção de importação de arquivos. Em um sentido geral, podemos assumir que o sistema operacional força o arquivo a chamar LoadLibrary, passando todos os nomes de biblioteca escritos na seção de importação. Como em 99,9% dos casos existem nomes e não caminhos, quando o aplicativo é iniciado, todas as bibliotecas carregadas serão pesquisadas no sistema.

Na lista de locais de pesquisa da DLL, dois pontos são realmente importantes para nós - 1 e 6. Se colocarmos version.dll na mesma pasta de onde o arquivo é iniciado, em vez do sistema, o carregado será carregado. Essa situação quase nunca é encontrada, porque, se houver uma oportunidade de colocar uma biblioteca, provavelmente será possível substituir o próprio arquivo executável. Mas ainda assim, essas situações são possíveis. Por exemplo, se o arquivo executável estiver localizado em uma pasta gravável e for um serviço com inicialização automática, ele não poderá ser alterado enquanto o serviço estiver em execução. Ou o arquivo iniciado é verificado externamente pela soma de verificação antes de iniciar, e a substituição do arquivo ainda não é uma opção. Mas colocar a biblioteca ao lado dela será bastante real.

Você pode não conseguir criar arquivos ao lado de arquivos executáveis, mas pode criar pastas. Nessa situação, o mecanismo de redirecionamento WinSxS (também conhecido como "DotLocal") pode funcionar.

Brevemente sobre DotLocal
O manifesto do arquivo pode conter uma dependência na biblioteca de uma versão específica. Nesse caso, ao iniciar o arquivo executável (por exemplo, application.exe), o sistema operacional verificará a existência de uma pasta chamada application.exe.local na mesma pasta que o próprio arquivo. Essa pasta deve ter uma subpasta com um nome complexo, como amd64_microsoft.windows.common-controls_6595b64144ccf1df_6.0.9600.19291_none_6248a9f3ecb5e89b, na qual já existe uma biblioteca comctl32.dll. O nome da biblioteca e as informações para o nome da pasta devem ser indicados no manifesto. Aqui está apenas um exemplo do primeiro processo encontrado. Se não houver pastas ou arquivos, a biblioteca será retirada de C: \ Windows \ WinSxS. No exemplo, C: \ Windows \ WinSxS \ amd64_microsoft.windows.common-controls_6595b64144ccf1df_6.0.9600.19291_none_6248a9f3ecb5e89b \ comctl32.dll.

Mas isso é mais a exceção do que a regra. Mas as situações em que a pesquisa dll atinge o sexto número na lista são bastante reais. Se o aplicativo tentar carregar uma dll que não esteja no sistema ou ao lado do arquivo, todas as pesquisas subirão para 6 pontos, que podem ser pastas graváveis.

Por exemplo, uma instalação típica do Python ocorre com mais freqüência na pasta C: \ Python (ou fechada). O próprio instalador python sugere adicionar suas pastas à variável de sistema PATH. Como resultado, temos um bom trampolim para iniciar um ataque - a pasta é gravável por todos os usuários e qualquer tentativa de carregar uma biblioteca inexistente irá para a pesquisa de caminho do PATH.

Agora que a teoria foi concluída, considere a criação da carga útil - as próprias bibliotecas de proxy.

A primeira opção Biblioteca de proxy honesta


Vamos começar com uma relativamente simples - criaremos uma biblioteca de proxy honesta. Honestidade, neste caso, implica que todas as funções na DLL serão registradas explicitamente e, para cada função, uma chamada de função com o mesmo nome da biblioteca original será gravada. Trabalhar com essa biblioteca será completamente transparente para o código chamado: se ele chamar alguma função, receberá a resposta correta, o resultado e tudo o que deve acontecer lado a lado.

Aqui está um link para o exemplo finalizado ( github ) da biblioteca version.dll.

Destaques do código:

  • Todos os protótipos de funções da tabela de exportação da biblioteca original são descritos honestamente.
  • A biblioteca original é carregada e todas as chamadas para nossas funções são lançadas nela.

Convenientemente , o aplicativo continua funcionando corretamente, sem experimentar "efeitos especiais". É inconveniente que eu tenha que escrever um monte de código uniforme para cada uma das funções, além disso, verificando cuidadosamente a coincidência dos protótipos.

A segunda opção Simplifique a escrita de código


Ao lidar com uma biblioteca como version.dll, onde a tabela de importação é pequena, existem apenas 17 funções e os protótipos são simples, uma biblioteca de proxy honesta é uma boa opção.



Mas se o proxy da biblioteca, por exemplo, bcrypt, tudo fica mais complicado. Aqui está sua tabela de importação:



57 recursos! E aqui estão alguns exemplos de protótipos:




Digamos apenas que nada é impossível, mas fazer um proxy honesto para essa biblioteca não é muito agradável.

Você pode simplificar o código se enganar um pouco com as funções. Declararemos todas as funções da biblioteca como __declspec (naked), e no corpo usaremos o código do assembler que simplesmente faz jmp na função da biblioteca original. Isso nos permitirá não usar protótipos longos, mas colocar anúncios simples em qualquer lugar sem exibir parâmetros:

void foo ()

Quando o aplicativo chama nossa função, a biblioteca proxy não realiza nenhuma manipulação com o registrador e a pilha, permitindo que a função original faça todo o trabalho como deveria.

Um exemplo ( github ) da biblioteca version.dll com essa abordagem.

Destaques:

  • A biblioteca original é carregada e todas as chamadas para nossas funções são lançadas nela. Os corpos de função e o carregamento são agrupados em macros.

Operação conveniente e correta do aplicativo e o fato de que mesmo um grande número de funções é facilmente descrito, graças às macros. É inconveniente que rake bastante inesperado em x64. O Visual Studio (em algum lugar desde 2012, se bem me lembro) proíbe o uso de inserções nuas e asm no código de 64 bits. Ao gravar um proxy do zero, é necessário que cada função verifique se está descrita no arquivo def, se o original está carregado e se o corpo da função está descrito.

A terceira opção. Nós jogamos fora o corpo em geral


Usar nu sugere mais uma opção. Você pode criar uma tabela de importação, que para todas as funções se referirá a uma linha de código real:

void nop () {}

Essa biblioteca será carregada pelo aplicativo, mas não funcionará. Ao chamar qualquer uma das funções, a pilha provavelmente será rasgada ou ocorrerá outra sujeira. Mas isso nem sempre é ruim - se, por exemplo, o objetivo de uma injeção de dll é simplesmente executar o código com os direitos necessários, basta executar a carga da biblioteca proxy DllMain e encerrar o aplicativo silenciosamente imediatamente. Nesse caso, não será feita uma chamada real para as funções e não haverá erros de queda.

Um exemplo em um github , novamente para version.dll.

Destaques do código:

  • Todas as funções do arquivo def se referem a uma função nop.

Convenientemente, essa biblioteca proxy é gravada apenas por alguns minutos. É inconveniente que o aplicativo chamado pare de funcionar.

A quarta opção. Leve utilitários prontos


Escrever uma dll é bom, mas nem sempre é conveniente e nem muito rápido, portanto, você deve considerar as opções automatizadas.

Você pode seguir o caminho dos vírus antigos - pegue a biblioteca cujos proxies queremos criar, crie uma seção executável do código, anote a carga útil lá e altere o ponto de entrada para esta seção. Não é a maneira mais fácil, porque você pode acidentalmente quebrar algo, você precisa escrever no assembler, lembre-se do dispositivo do arquivo PE. Este não é o nosso caminho.

Para operar o dll hijack, adicionaremos outro dll hijack.



Isso é relativamente fácil de fazer. Copiamos a biblioteca cujo proxy queremos criar e adicionamos alguma dll com uma função arbitrária à tabela de importação dessa cópia. Agora o download seguirá a cadeia - no início do arquivo executável, a DLL do proxy será carregada, o que carregará a própria biblioteca especificada.

“Ei, você substituiu o carregamento de uma biblioteca por outra. Qual é o objetivo? Mesmo assim, será necessário codificar dll! ". Tudo está correto, mas ainda há um sentido. Agora haverá menos requisitos para uma biblioteca com uma carga útil. Você pode especificar qualquer nome, o principal é exportar apenas uma função, que pode ter qualquer protótipo. Digite o nome principal da biblioteca e a função na tabela de importação.

Uma biblioteca com carga útil pode ser uma para todas as ocasiões.

Você pode modificar a tabela de importação com muitos editores de PE, por exemplo, CFF explorer ou pe-bear. Para mim, escrevi um pequeno utilitário em C # que corrige uma tabela sem gestos desnecessários. Fontes no github , binar na seção Release .

Conclusão


No artigo, tentei divulgar os métodos básicos para a criação de dll proxy, que eu mesmo usei. Resta apenas dizer como se defender.

Não há muitas recomendações universais:

  • Não armazene arquivos executáveis, especialmente aqueles executados com permissões altas, em pastas graváveis ​​para os usuários.
  • É melhor primeiro encontrar e verificar a existência da biblioteca antes de executar o LoadLibrary.
  • Veja os métodos de proteção existentes disponíveis no sistema operacional. Por exemplo, no Windows 10, você pode definir o sinalizador PreferSystem32 para que a pesquisa da DLL não comece com a pasta do arquivo executável, mas com o system32.

Obrigado por sua atenção, ficarei feliz em ouvir perguntas, sugestões, sugestões e comentários.

UPD: Seguindo o conselho dos comentaristas, lembro que você precisa escolher uma biblioteca com cuidado e cuidado. Se a biblioteca estiver incluída na lista KnownDlls ou o nome for semelhante ao MinWin (ApiSetSchema, api-ms-win-core-console-l1-1-0.dll - isso é tudo), provavelmente não será possível interceptá-lo devido aos recursos de processamento DLLs no sistema operacional.

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


All Articles