Fazendo qualquer processo funcionar com NTFS transacional: minha primeira etapa na criação de uma sandbox para Windows

TransactionMaster Há um módulo no kernel do Windows que é responsável por oferecer suporte ao agrupamento de operações de arquivo em alguma entidade chamada transação . As ações nessa entidade são isoladas e atômicas: elas podem ser aplicadas tornando-as permanentes ou revertidas . Muito conveniente ao instalar programas, concorda? Sempre passamos de um estado acordado para outro e, se algo der errado, todas as alterações serão revertidas.


Desde que soube do suporte a essa funcionalidade, sempre quis ver o mundo por dentro dessas transações. E você sabe o que: achei um método simples e verdadeiramente maravilhoso para fazer qualquer processo funcionar dentro de uma transação de arquivo, mas as margens do livro são muito estreitas para ele . Na maioria dos casos, isso nem exige privilégios administrativos.


Vamos descobrir como funciona, experimentar meu programa e entender o que são as caixas de areia.


Repositório


Para quem está ansioso para experimentar: TransactionMaster no GitHub .


Teoria


O suporte para NTFS transacional, ou TxF , apareceu no Windows Vista e tornou possível simplificar significativamente o código responsável pela recuperação de erros no processo de atualização do software e do próprio sistema operacional. De fato, a tarefa de restauração foi transferida para o kernel do sistema operacional, que começou a aplicar a semântica completa do ACID às operações de arquivo - basta perguntar.


Para suportar essa tecnologia, foram adicionadas novas funções de API que duplicaram a funcionalidade existente, adicionando um novo parâmetro - uma transação. A transação em si se tornou um dos muitos objetos do kernel no sistema operacional, junto com arquivos, processos e objetos de sincronização. No caso mais simples, a sequência de ações ao trabalhar com transações consiste em criar um objeto de transação chamando CreateTransaction , trabalhando com arquivos (usando funções como CreateFileTransacted , MoveFileTransacted , DeleteFileTransacted e similares) e aplicando / revertendo a transação usando CommitTransaction / RollbackTransaction .


Agora vamos dar uma olhada na arquitetura desses recursos. Sabemos que a camada API documentada, de bibliotecas como kernel32.dll , não transfere diretamente o controle para o kernel do sistema operacional, mas refere-se à camada de abstração subjacente no modo de usuário - ntdll.dll , que já faz uma chamada de sistema. E aqui uma surpresa nos espera: simplesmente não há duplicação de funções para trabalhar com arquivos no contexto de transações no ntdll , como no kernel .


Camadas de API

No entanto, os protótipos dessas funções da API nativa não mudaram desde tempos imemoriais, o que significa que eles aprenderão de algum outro lugar no contexto de qual transação executar a operação. Mas de onde? A resposta é que cada encadeamento possui um campo especial no qual o identificador da transação atual é armazenado. A área da memória em que está localizada é chamada TEB , o bloco do ambiente de fluxo. De coisas conhecidas, o último código de erro e identificador de fluxo também são armazenados lá.


Portanto, funções com o sufixo *Transacted definem o campo da transação atual, chamam uma função semelhante sem sufixo e, em seguida, restaura o valor anterior. Eles fazem isso usando um par de RtlSetCurrentTransaction / RtlSetCurrentTransaction do ntdll . O código das próprias funções é muito direto, com exceção do caso do WoW64 , que será discutido abaixo.


O que tudo isso significa para nós? Alterando uma variável na memória do processo, podemos controlar no contexto de qual transação ela está trabalhando com o sistema de arquivos. Você não precisa definir traps e interceptar chamadas de função, apenas entregue o descritor de transação ao processo de destino e corrija alguns bytes em sua memória para cada um dos encadeamentos. Isso parece elementar, vamos fazê-lo!


Armadilhas


Os primeiros experimentos mostraram que a idéia é viável: o Far Manager , que eu uso em vez do Windows Explorer, sobrevive perfeitamente à substituição de transações dinamicamente e permite que você veja o mundo em seu contexto. Mas também havia programas que constantemente criam novos threads para operações de arquivo. E no cenário original, essa é uma lacuna, pois não é muito conveniente rastrear a criação de threads em outro processo (sem mencionar o fato de que "atraso" é crítico aqui). Um exemplo de um aplicativo da segunda classe é o WinFile recentemente portado.


DLL de rastreamento


Felizmente, o rastreamento síncrono da criação do encadeamento e a configuração subsequente das transações para eles são completamente elementares a partir do processo de destino. Basta incorporar uma DLL nela, e o carregador do módulo chama seu ponto de entrada com o parâmetro DLL_THREAD_ATTACH toda * vez ao criar um novo encadeamento. Ao implementar essa funcionalidade, corrigi a compatibilidade com uma dúzia de outros programas.


* Tecnicamente, uma chamada nem sempre funciona, e esse comportamento às vezes pode ser observado na interface do meu programa. Na maioria das vezes, as exceções são encadeamentos do pool de trabalho do próprio carregador de módulos. O problema é que as bibliotecas DLL são notificadas sob o bloqueio do carregador de inicialização e isso significa: você não pode carregar novos módulos no momento. E os threads do carregador, como você sabe, fazem exatamente isso, paralelizando o acesso ao sistema de arquivos. É fornecida uma exceção para esses casos: se você especificar THREAD_CREATE_FLAGS_SKIP_THREAD_ATTACH como um sinalizador ao chamar NtCreateThreadEx , poderá evitar adicionar um novo encadeamento às DLLs existentes e, respectivamente, deadlocks. Sobre isso é o que acontece aqui.


Iniciar o Explorer


Resta a terceira e última categoria de programas que ainda falham ao tentar fazê-los funcionar em uma transação. Um desses programas é o Windows Explorer. Não consigo diagnosticar o problema com precisão, mas o aplicativo é complicado e a troca a quente dentro da transação não o afeta muito. Talvez o motivo seja o fato de ter muitos descritores de arquivos abertos, alguns dos quais deixam de ser válidos no novo contexto. Ou talvez seja outra coisa. Nessas situações, reiniciar o processo ajuda, para que ele funcione desde o início da transação. Então, nenhuma inconsistência deve surgir.


E, portanto, adicionei a capacidade de iniciar novos processos no programa, para os quais a transação e o rastreamento de novos fluxos são configurados antes mesmo de chegar ao ponto de entrada, enquanto o processo está em pausa. E você sabe o que, funcionou! É verdade que, como o Explorer usa ativamente objetos COM fora do processo, a visualização é interrompida ao mover arquivos. Mas, caso contrário, tudo é estável.


O que há com o WoW64?


Esse subsistema para iniciar programas de 32 bits em sistemas de 64 bits é uma ferramenta extremamente conveniente, mas a necessidade de levar em conta seus recursos geralmente complica a programação do sistema. Rtl[Get/Set]CurrentTransaction acima que o comportamento de Rtl[Get/Set]CurrentTransaction marcadamente diferente no caso de processos similares. A razão para isso reside no fato de que os threads nos processos WoW64 têm até dois blocos de ambiente. Eles têm tamanhos diferentes do ponteiro, e é desejável mantê-los em um estado consistente, embora, no caso de transações, o TEB de 64 bits tenha precedência. Quando estabelecemos transações remotamente, devemos reproduzir o comportamento dessas funções. Não é difícil, mas você não deve esquecê-lo, e detalhes podem ser encontrados aqui . Finalmente, os processos WoW64 precisam de uma cópia adicional de 32 bits da nossa DLL de rastreamento.


Problemas não resolvidos


Forçado a sofrer - o primeiro cenário que vem à mente, a saber, o lançamento de instaladores de programas nesse modo - ainda não está operacional. Primeiramente, ele não está configurado para capturar processos filhos na mesma transação. Existem várias soluções aqui, estou trabalhando nisso. Mas se o aplicativo criar vários processos, ainda é muito cedo para usá-lo em combinação com transações.


Em segundo lugar, o caso com arquivos executáveis ​​que não existem fora da transação merece atenção especial. Lembro-me de que havia algum tipo de vírus que enganava antivírus ingênuos dessa maneira: era descompactado em uma transação, se lançava e depois revertia a transação. Existe um processo, mas não há arquivo executável. O antivírus pode decidir que não há nada para verificar e ignorar a ameaça. Também precisamos trabalhar em soluções criativas, porque, por algum motivo, NtCreateUserProcess (e, consequentemente, CreateProcess ) ignora a transação atual. Obviamente, o NtCreateProcessEx sempre permanece, mas espera-se muita confusão para corrigir problemas de compatibilidade. Vou pensar em alguma coisa.


E onde estão as caixas de areia?


Dê uma olhada na foto. Aqui, três programas diferentes mostram o conteúdo da mesma pasta em três transações diferentes. Legal, né?


Um olhar de dentro da transação

E, no entanto, meu programa não é de modo algum uma caixa de areia, falta um detalhe importante - a fronteira de segurança . Obviamente, isso não impede que algumas empresas vendam artesanato semelhante sob o disfarce de caixas de areia de pleno direito, uma vergonha para elas, o que posso dizer. E, apesar de isso parecer completamente impossível, como podemos impedir que um programa altere uma variável em nossa memória, mesmo se formos um depurador? - Tenho um truque delicioso que me permitirá concluir o que iniciei e criar a primeira caixa de proteção que conheço, que não exigirá um driver, mas virtualizará o sistema de arquivos. Até lá, aguarde atualizações, use o Sandboxie e experimente a tecnologia AppContainer . Obrigado pela atenção.


Repositório do projeto no GitHub: TransactionMaster .
O mesmo artigo em inglês .

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


All Articles