
Um descompactador e registrador automático para arquivos direcionados ao DotNet Framework
Um descompactador e registrador automático para arquivos direcionados ao DotNet Framework! Esta ferramenta foi apresentada no Black Hat USA 2022. No Black Hat Asia 2023, foram publicados DotDumperGUI e DotDumperNative, juntamente com a versão 1.1-stable do DotDumper. Essas três ferramentas devem ser usadas em conjunto, onde o DotDumper 1.1-stable requer que as DLLs do DotDumperNative estejam presentes, enquanto o DotDumperGUI é uma interface gráfica para abrir e filtrar a saída JSON das execuções do DotDumper.
A detecção e classificação automática de qualquer arquivo de maneira confiável é frequentemente considerada o santo graal da análise de malware. As dificuldades para chegar lá são muitas, razão pela qual a criação de tal sistema é altamente valorizada. Quando se trata de binários direcionados ao DotNet, nossa nova ferramenta de código aberto, DotDumper, visa auxiliar em várias etapas cruciais ao longo do caminho: registrar atividades (em memória), despejar segmentos de memória interessantes e extrair características da amostra fornecida.
Em resumo, a descompactação manual é um processo tedioso que consome uma quantidade desproporcional de tempo dos analistas. Binários ofuscados aumentam ainda mais o tempo que um analista deve gastar para descompactar um determinado arquivo. Ao escalar isso, as organizações precisam de inúmeros analistas que dissecam malware diariamente, provavelmente em combinação com uma sandbox escalável. O valioso tempo perdido poderia ser usado para investigar campanhas ou amostras interessantes para descobrir novas ameaças, em vez do malware genérico e mundano que é amplamente disseminado. Afinal, os analistas procuram as poucas agulhas no palheiro.
Então, qual diferença o DotDumper faz? Executar uma amostra de malware baseada em DotNet através do DotDumper fornece arquivos de registro de chamadas de função cruciais, contextualizadoras e comuns em três formatos (texto simples legível por humanos, JSON e XML), além de cópias de segmentos úteis na memória. Dessa forma, um analista pode folhear o registro de chamadas de função. Além disso, os arquivos despejados podem ser escaneados para classificá-los, fornecendo informações adicionais sobre a amostra de malware e os dados que ela contém. Isso reduz o tempo vital para os processos de triagem e resposta a incidentes, e libera o tempo dos analistas de SOC e pesquisadores para necessidades de análise mais sofisticadas.
Para registrar e despejar as chamadas de função contextualizadoras e seus resultados, o DotDumper usa uma mistura de reflexão e hooks gerenciados, todos escritos em C# puro. Abaixo, os principais recursos serão destacados e elaborados, em combinação com trechos dos resultados do DotDumper de uma amostra do stealers AgentTesla empacotado, cujos hashes estão abaixo.
| Tipo de hash | Valor do hash |
|---|---|
| SHA-256 | b7512e6b8e9517024afdecc9e97121319e7dad2539eb21a79428257401e5558d |
| SHA-1 | c10e48ee1f802f730f41f3d11ae9d7bcc649080c |
| MD-5 | 23541daadb154f1f59119952e7232d6b |
O DotDumper é acessível através de uma interface de linha de comando, com uma variedade de argumentos. A imagem abaixo mostra o menu de ajuda. Observe que nem todos os argumentos serão discutidos, mas sim os mais usados.

O requisito mínimo para executar uma amostra é fornecer o argumento "-file", juntamente com um nome de arquivo ou caminho de arquivo. Se um caminho completo for fornecido, ele será usado. Se um nome de arquivo for fornecido, o diretório de trabalho atual é verificado, bem como a pasta do executável do DotDumper.
A menos que um nome de diretório seja fornecido, o nome da pasta "-log" é definido igual ao nome do arquivo da amostra sem a extensão (se houver). A pasta está localizada na mesma pasta onde o DotDumper reside, onde os logs e arquivos despejados serão salvos.
No caso de uma biblioteca, ou de um ponto de entrada alternativo em um binário, deve-se substituir o ponto de entrada usando "-overrideEntry true". Além disso, é necessário fornecer a classe totalmente qualificada, que inclui o namespace, usando "-fqcn My.NameSpace.MyClass". Isso informa ao DotDumper qual classe selecionar, que é onde o nome da função fornecida (usando "-functionName MyFunction") é recuperado.
Se a função selecionada exigir argumentos, é necessário fornecer o número de argumentos usando "-argc" e o número de argumentos necessários. Os tipos e valores dos argumentos devem ser fornecidos como "string|myValue int|9". Observe que, quando espaços são usados nos valores, o argumento na interface de linha de comando precisa ser encapsulado entre aspas para garantir que seja passado como um único argumento.
Outras opções menos usadas, como "-raceTime" ou "-deprecated", são seguras em suas configurações padrão, mas podem exigir ajustes no futuro devido a mudanças no DotNet Framework. Elas estão atualmente expostas na interface de linha de comando para permitir alterações facilmente, se necessário, mesmo que se esteja usando uma versão mais antiga do DotDumper quando chegar a hora.
Registro e despejo são os dois recursos principais do DotDumper. Para minimizar o tempo que a análise leva, o registro deve fornecer contexto ao analista. Isso é feito fornecendo ao analista as seguintes informações para cada chamada de função registrada:
Observe que, para cada arquivo despejado, o nome do arquivo é igual ao hash SHA-256 do arquivo.
Para esclarecer o acima, um trecho de um log é fornecido abaixo. O trecho mostra os detalhes para a amostra AgentTesla mencionada, onde ela carrega o segundo estágio usando a função Assembly.Load do DotNet.
![O log para uma chamada de função Assembly.Load(byte[] rawAssembly) interceptada](images/assembly_load.png)
Primeiro, a hora do sistema local é fornecida, juntamente com o tipo de retorno, nome e argumento(s) da função original. Segundo, o stack trace é fornecido, onde mostra que a função principal da amostra leva a um construtor, inicializa os componentes e chama duas funções personalizadas. A função Assembly.Load foi chamada de dentro de "NavigationLib.TaskEightBestOil.GGGGGGGGGGGGGGGGGGGG(String str)". Isso fornece contexto para o analista encontrar o código em torno desta chamada se for de interesse.
Em seguida, são fornecidas informações sobre a ordem de chamada do assembly. Quanto mais estágios são carregados, mais complexo se torna ver através de quais estágios a chamada ocorreu. Normalmente, espera-se que um estágio carregue o próximo, mas em alguns casos, estágios posteriores utilizam estágios anteriores de forma não linear. Além disso, informações sobre o assembly de origem são fornecidas para enriquecer ainda mais os dados para o analista.
Em seguida, o hash pai é fornecido. O pai de um estágio é o estágio anterior, que neste exemplo ainda não está presente. O estágio recém-carregado terá este estágio como seu pai. Isso permite que o analista correlacione eventos mais facilmente.
Finalmente, o tipo e valor de retorno da função são armazenados, juntamente com o tipo, nome e valor de cada argumento que é passado para a função hookeada. Se alguma variável for maior que 100 bytes de tamanho, ela é armazenada no disco. Uma referência é então inserida no log para referenciar o arquivo, em vez de mostrar o valor. O limite foi definido para evitar soluços na impressão do log, pois alguns arrays têm milhares de índices de tamanho.
De acordo com a documentação da Microsoft, a reflexão é melhor resumida como "[...] fornece objetos que encapsulam assemblies, módulos e tipos". Em resumo, isso permite a criação e invocação dinâmica de classes e funções DotNet da amostra de malware. O DotDumper contém um carregador reflexivo que permite a um analista carregar e analisar tanto executáveis quanto bibliotecas, desde que sejam baseados no DotNet Framework.
Para utilizar o carregador, deve-se optar por sobrescrever o ponto de entrada na interface de linha de comando, especificar a classe (incluindo o namespace onde reside) e o nome da função dentro de um determinado arquivo. Opcionalmente, pode-se fornecer argumentos para a função especificada, para todos os tipos nativos e arrays dos mesmos. Exemplos de tipos nativos são int, string, char, e arrays como int[], string[] e char[]. Todos os argumentos devem ser fornecidos via interface de linha de comando, onde tanto o tipo quanto o valor devem ser especificados.
Não sobrescrever o ponto de entrada resulta na utilização do ponto de entrada padrão. Por padrão, um array de strings vazio é passado para a função principal da amostra, como se a amostra fosse executada sem argumentos.
Além disso, a reflexão é frequentemente usada por carregadores para invocar uma determinada função em uma determinada classe no próximo estágio. Às vezes, argumentos também são passados, que são usados posteriormente para descriptografar um recurso. Na amostra AgentTesla mencionada, esse cenário exato ocorre. Os hooks relacionados a invoke do DotDumper registram essas ocorrências, como pode ser visto abaixo.

O nome da função na primeira linha não é uma função interna do DotNet Framework, mas sim uma chamada para uma função específica no segundo estágio. Os tipos e nomes dos três argumentos estão listados na assinatura da função. Seus valores podem ser encontrados na seção de informações do argumento da função. Isso permitiria a um analista carregar o segundo estágio em um carregador personalizado com os valores fornecidos para os argumentos, ou até mesmo fazer isso usando o DotDumper carregando o estágio previamente despejado e fornecendo os argumentos.
Antes de entrar nos hooks gerenciados, é preciso entender como os hooks funcionam. Existem duas variáveis principais a serem consideradas aqui: a função alvo e uma função controlada que é chamada de hook. Simplificando, a memória na função alvo (ou seja, Assembly.Load) é alterada para saltar para o hook. Como resultado, o fluxo de execução do programa é desviado. O hook pode então realizar ações arbitrárias, opcionalmente chamar a função original, após o que retorna a execução para o chamador juntamente com um valor de retorno, se necessário. O diagrama abaixo ilustra esse processo.

Saber o que são hooks é essencial para entender o que são hooks gerenciados. O código gerenciado é executado em um ambiente virtual e gerenciado, como o runtime do DotNet ou a máquina virtual do Java. Obter o endereço de memória onde a função gerenciada reside é diferente de uma linguagem não gerenciada como C. Uma vez que os endereços de memória corretos para ambas as funções foram obtidos, o hook pode ser configurado acessando diretamente a memória usando C# inseguro, juntamente com o serviço de interoperabilidade do DotNet para chamar funcionalidades da API nativa do Windows.
Desde o DotDumper v1.1-stable, o DotDumper também pode hookear funções não gerenciadas (ou nativas, se preferir). O redirecionamento de função pode ser qualquer combinação de funções gerenciadas e não gerenciadas, com uma ressalva importante. Qualquer função não gerenciada que use um hook gerenciado não conseguirá ler corretamente os valores da pilha. Como tal, um componente nativo é necessário (chamado DotDumperNative). Este componente se comunica via um pipe nomeado com o próprio DotDumper, utilizando assim seu sistema de registro centralizado, enquanto intercepta chamadas não gerenciadas com acesso aos argumentos da função encontrados na pilha.
Como o DotDumper é escrito em C# puro sem dependências externas, pode-se facilmente estender a estrutura usando o Visual Studio. O código é documentado neste blog, no GitHub, e em classes, funções e inline no código-fonte. Isso, em combinação com o esquema de nomenclatura claro, permite que qualquer um modifique a ferramenta conforme achar necessário, minimizando o tempo e esforço que se precisa gastar para entender a ferramenta. Em vez disso, permite que desenvolvedores e analistas concentrem seus esforços na melhoria da ferramenta.
Embora o recurso de suporte a timeout de sandbox não tenha sido alterado nem modificado, ele não foi documentado antes. Como o DotDumper executa o arquivo que lhe é fornecido, sua execução continuará até que a amostra termine. O malware frequentemente entra em um estado de "espera", onde uma determinada condição deve ser atendida antes que ele reative, ou o malware continua a execução em um processo diferente (com a ajuda de injeção de processo). Por exemplo, uma nova thread criada em um processo oco só retorna quando termina.
Para evitar paralisação, o DotDumper possui um manipulador de estagnação. Cada vez que os resultados de um hook são registrados, um contador é incrementado. Se este contador não mudar após três intervalos consecutivos de 20 segundos, o DotDumper assume que um estado de estagnação foi encontrado. Nesse caso, a hora do sistema é ajustada para 30-12-2200 12:00 para forçar o timeout da sandbox, após o que ele notifica o analista através do registrador e se encerra. O timeout da sandbox ocorre quando o tempo decorrido da data atual até a data recém-definida excede os 5 ou 10 minutos habituais. Mesmo que o tempo de execução da sandbox seja definido em horas ou dias, ele é excedido.
A razão para interromper a análise mais cedo é economizar tempo e recursos, já que uma submissão com um timeout de 10 minutos pode levar apenas 2 minutos para ser executada. Dessa forma, seria possível executar várias amostras no mesmo intervalo de tempo com o manipulador de estagnação. Uma visão geral da lógica do manipulador de estagnação é mostrada abaixo.

Com o objetivo e os recursos do DotDumper claros, pode parecer que há sobreposição com ferramentas públicas conhecidas como ILSpy, dnSpyEx, de4dot ou pe-sieve. Observe que não há intenção de proclamar que uma ferramenta é melhor que outra, mas sim como as ferramentas diferem.
O objetivo do DotDumper é registrar e despejar chamadas de função cruciais, contextualizadoras e comuns de amostras direcionadas ao DotNet. O ILSpy é um desmontador e descompilador DotNet, mas não permite a execução do arquivo. O dnSpyEx (e seu predecessor dnSpy) utilizam o ILSpy como componente de desmontagem e descompilação, enquanto adicionam um depurador. Isso permite inspecionar e manipular a memória manualmente. O de4dot é usado exclusivamente para desofuscar binários DotNet, melhorando a legibilidade do código para olhos humanos. A última ferramenta nesta comparação, pe-sieve, destina-se a detectar e despejar malware de processos em execução, desconsiderando a linguagem de programação utilizada. A tabela abaixo fornece uma visão geral gráfica das ferramentas mencionadas.

O DotDumper está sob constante revisão e desenvolvimento, tudo focado em duas áreas principais de interesse: correção de bugs e adição de novos recursos. Durante o desenvolvimento, o código foi testado, mas devido à injeção de hooks nas funções do DotNet Framework, que podem estar sujeitas a alterações, é bem possível que haja bugs no código. Qualquer pessoa que encontrar um bug é incentivada a abrir uma issue no repositório do GitHub, que será então analisada. A sugestão de novos recursos também é possível através do repositório do GitHub. Para aqueles com uma conta no GitHub, ou para aqueles que preferem não interagir publicamente, fiquem à vontade para me enviar uma mensagem privada no meu Twitter.
Escusado será dizer que, se você usou o DotDumper durante uma análise, ou o usou de forma criativa, sinta-se à vontade para entrar em contato publicamente ou em particular! Não há nada como ouvir sobre o uso de uma ferramenta caseira!
Há mais novidades por vir para o DotDumper, e uma atualização será enviada à comunidade assim que estiver disponível!