Como fazer com que qualquer processo funcione com o NTFS transacional: minha primeira etapa para escrever uma sandbox para Windows

TransactionMaster Um dos módulos no kernel do Windows fornece suporte para combinar um conjunto de operações de arquivo em uma entidade conhecida como transação . Assim como nos bancos de dados, essas entidades são isoladas e atômicas . Você pode fazer algumas alterações no sistema de arquivos que não serão visíveis fora até que você as confirme . Ou, como alternativa, você sempre pode reverter tudo. De qualquer forma, você age sobre o grupo de operações como um todo. Precisamente o que era necessário para preservar a consistência ao instalar o software ou atualizar nossos sistemas, certo? Se algo der errado - o instalador ou todo o sistema travar - a transação será revertida automaticamente.


Desde a primeira vez que vi um artigo sobre esse mecanismo incrível, sempre me perguntei como seria o mundo por dentro. E você sabe o que? Acabei de descobrir uma abordagem verdadeiramente maravilhosa para forçar qualquer processo a operar dentro de uma transação predefinida, que essa margem é muito estreita para conter . Além disso, na maioria das vezes, nem exige privilégios administrativos.


Vamos então falar sobre os componentes internos do Windows, experimentar uma nova ferramenta e responder a uma pergunta: o que isso tem a ver com caixas de areia?


Repositório


Aqueles que desejam começar a experimentar imediatamente são bem-vindos na página do projeto no GitHub: TransactionMaster .


Teoria


A introdução do NTFS transacional, também conhecido como TxF , no Windows Vista foi um passo revolucionário no sentido de manter a consistência do sistema e, portanto, a estabilidade. Ao expor essa funcionalidade diretamente aos desenvolvedores, a Microsoft tornou possível simplificar dramaticamente o tratamento de erros em todos os componentes responsáveis ​​pela instalação e atualização do software. A tarefa de manter um plano de backup para todas as possíveis falhas no sistema de arquivos tornou-se um trabalho do próprio sistema operacional, que começou a fornecer uma semântica de ACID com todos os recursos sob demanda.


Para fornecer esse novo instrumento, a Microsoft introduziu um conjunto de funções de API que duplicavam a funcionalidade existente, mas dentro de um contexto de transações. A transação em si se tornou um novo objeto de kernel, juntamente com os existentes, como arquivos, processos e primitivas de sincronização. No cenário mais simples, o aplicativo cria uma nova transação usando CreateTransaction , executa as operações necessárias ( CreateFileTransacted , MoveFileTransacted , DeleteFileTransacted etc.) e as confirma ou reverte com CommitTransaction / RollbackTransaction .


Vamos dar uma olhada na arquitetura dessas novas funções. Sabemos que a camada oficial da API de bibliotecas como o kernel32.dll não invoca o kernel diretamente, mas converte os parâmetros e encaminha a chamada para ntdll.dll . Que, então, emite um syscall. Surpreendentemente, não há sinal de nenhuma função -Transacted adicional no lado ntdll e no kernel da chamada.


Camadas de API

As definições dessas funções da API nativa não mudaram em décadas, portanto, não há parâmetro extra para especificar uma transação. Como o kernel sabe qual usar então? A resposta é simples, mas promissora: cada segmento tem um campo designado, onde armazena um identificador para a transação atual. Essa variável reside em uma região específica da memória denominada TEB - Thread Environmental Block. Quanto a outros campos conhecidos localizados aqui também, posso nomear o último código de erro e o ID do encadeamento .


Portanto, todas as funções com o sufixo -Transacted definem o campo de transação atual no TEB, chamam a API não transacionada correspondente e restauram o valor anterior. Para atingir esse objetivo, eles usam um par de rotinas bastante diretas, chamadas RtlGetCurrentTransaction / RtlSetCurrentTransaction from ntdll . Eles fornecem um nível suficiente de abstração, que é útil no caso do WoW64 , mais sobre isso posteriormente.


O que isso significa para nós? Alterando uma variável na memória, podemos controlar, em um contexto de qual transação o processo acessa o sistema de arquivos. Não há necessidade de instalar ganchos ou retornos de chamada no modo kernel, tudo o que precisamos é entregar o identificador para o processo de destino e modificar alguns bytes de memória por cada thread. Parece surpreendentemente fácil, mas o resultado deve ser surpreendente!


Armadilhas


O primeiro conceito de trabalho revelou muitos detalhes peculiares. Para minha grande satisfação, o Far Manager , que eu uso como um substituto para o Windows Explorer, está perfeitamente bem com a troca a quente de transações. Mas também vi alguns programas nos quais meu método não tinha efeito esperado, pois eles criam novos threads para operações de arquivo. Um exemplo do representante de segunda classe é o WinFile . Apenas como lembrete, a transação atual é um recurso por thread. Inicialmente, havia um buraco na plotagem, pois o rastreamento da criação de encadeamento fora do processo é bastante difícil, considerando a sensibilidade do tempo dessa operação.


DLL de rastreamento de threads


Felizmente, receber notificações síncronas sobre a criação de encadeamentos é extremamente simples dentro do contexto do processo de destino. Tudo o que precisamos é criar uma DLL, que propague a transação atual para novos threads, o carregador de módulos da ntdll cuidará do resto. Sempre que um novo thread chega ao processo, ele dispara nosso ponto de entrada com o parâmetro DLL_THREAD_ATTACH . Ao implementar essa funcionalidade, eu consertei a compatibilidade com vários programas diferentes.


* A rigor, esse retorno de chamada não ocorre em todas as condições possíveis. De vez em quando, você verá um ou dois encadeamentos auxiliares sem uma transação. Na maioria das vezes, esses são os threads do pool de trabalho do próprio carregador de módulos. O motivo é que as notificações de DLL ocorrem sob o bloqueio do carregador , o que implica uma variedade de limitações, incluindo a capacidade de carregar mais módulos. E é isso que esses threads precisam realizar, paralelizando o acesso ao arquivo enquanto isso. Portanto, existe uma exceção para evitar conflitos: se o chamador especificar o sinalizador THREAD_CREATE_FLAGS_SKIP_THREAD_ATTACH ao criar um thread com a ajuda de NtCreateThreadEx , os retornos de chamada de notificação da DLL não NtCreateThreadEx acionados.


Iniciando o Windows Explorer


Infelizmente, ainda existem alguns programas que não conseguem lidar bem com a troca a quente de transações, e o Windows Explorer é um deles. Não consigo diagnosticar o problema com segurança. É um aplicativo complexo que geralmente possui muitos identificadores abertos e, se o contexto de uma transação invalidar alguns deles, poderá resultar em uma falha. De qualquer forma, a solução universal para esses problemas é garantir que o processo seja executado dentro de um contexto consistente desde a primeira instrução executada.


Portanto, implementei uma opção para executar a injeção de DLL imediatamente ao criar um novo processo. E acabou por ser o suficiente para corrigir as falhas. Embora, como o Explorer use intensivamente COM fora de processo, a visualização e alguns outros recursos ainda não funcionem em arquivos modificados.


E o WoW64?


Os benefícios de compatibilidade que o subsistema Windows no Windows fornece são sinceramente notáveis. No entanto, levar em consideração suas especificidades geralmente se torna tedioso durante a programação do sistema. Rtl[Get/Set]CurrentTransaction anteriormente que o comportamento do Rtl[Get/Set]CurrentTransaction se torna um pouco mais complicado nesse caso. Como esses processos funcionam com um tamanho distinto de ponteiros que o restante do sistema, cada thread WoW64 mantém dois TEBs associados a ele: o próprio sistema operacional espera que ele seja de 64 bits e o aplicativo requer um de 32 bits, como bem para funcionar corretamente. E mesmo que, da perspectiva do kernel, o TEB nativo tenha precedência, há algum código extra nessas funções para garantir que os valores correspondentes sempre correspondam. De qualquer forma, é essencial ter em mente todas essas peculiaridades ao implementar novas funcionalidades .


Problemas não resolvidos


Por mais triste que seja, o primeiro cenário de uso que vem à nossa mente - instalar aplicativos nesse modo - não funciona bem por enquanto. Antes de tudo, os instaladores frequentemente criam processos suplementares e ainda não implementei a captura de processos filhos na mesma transação. Vejo várias maneiras de fazer isso, mas pode demorar um pouco. Outro grande problema surge quando tentamos executar binários que são descompactados durante a instalação e, portanto, não existem em nenhum outro lugar. Considerando que NtCreateUserProcess e, portanto, CreateProcess , ignoram a transação atual por algum motivo, resolver esse problema provavelmente exigirá alguma criatividade, combinada com vários truques sofisticados. Obviamente, sempre podemos confiar no NtCreateProcessEx como último recurso, mas a correção da compatibilidade pode se tornar um pesadelo nesse caso.


A propósito, isso me lembra uma história que li sobre um malware que conseguiu enganar alguns antivírus ingênuos aplicando uma abordagem semelhante. Ele costumava soltar uma carga útil em uma transação, iniciá-la e reverter as alterações para cobrir as faixas, permitindo que o processo fosse executado sem uma imagem no disco. Mesmo que a lógica de “nenhum arquivo - nenhuma ameaça” pareça tola, pode não ter sido o caso de alguns AVs, pelo menos há alguns anos atrás.


O que há com o Sandboxing?


Dê uma olhada nesta captura de tela abaixo. Você pode ver três programas discordando completamente do conteúdo da mesma pasta. Todos eles trabalham dentro de três transações diferentes. Esse é o poder do isolamento na semântica do ACID.


Isolamento de transação

Meu programa não é um sandbox; falta uma peça crucial - um limite de segurança . Eu sei que algumas empresas ainda conseguem vender produtos similares, apresentando-os como caixas de areia reais, com vergonha, o que posso dizer. E você pode pensar: como você pode transformá-lo em uma caixa de areia, mesmo sendo um depurador, não é possível impedir com segurança que um processo modifique uma variável que controla a transação, afinal ele reside em sua memória. É justo, é por isso que tenho que ter outro truque maravilhoso na manga, o que acabará por me ajudar a terminar este projeto e que não vou revelar por enquanto. Sim, estou planejando criar uma sandbox completamente no modo de usuário com virtualização do sistema de arquivos. Enquanto isso, use o Sandboxie e continue experimentando o AppContainers . Fique atento.


Repositório do projeto no GitHub: TransactionMaster .

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


All Articles