QILING é uma estrutura de código aberto que oferece a possibilidade de emular arquivos binários de diferentes formatos, por exemplo, PE, ELF, COM, entre outros. Ele é muito útil ao analisar uma amostra de malware e compreender melhor uma funcionalidade específica do código malicioso sem recorrer à execução completa da amostra.
Por sua vez, QILING faz uso de outra estrutura conhecida como Unicorn, que é um emulador de instruções de CPU de plataforma cruzada baseado no emulador QEMU. Assim, enquanto a Unicorn cuida de toda a emulação em um nível inferior, QILING cuida da interpretação do sistema operacional ao qual o arquivo a ser emulado pertence, podendo lidar com chamadas API ou chamadas de sistema (syscalls) e gerenciamento de memória, entre outras coisas.
Graças a isto, a estrutura consegue enganar o arquivo binário, fazendo-o acreditar que ele está sendo executado no sistema operacional para o qual foi projetado, quando na verdade não é este o caso e podemos acabar, por exemplo, "executando" um arquivo binário do Windows em um sistema operacional Linux.
Outras características do QILING incluem:
- Suporte multiplataforma: Windows, MacOS, Linux, BSD, UEFI, DOS, MBR, Ethereum Virtual Machine;
- Suporte para diferentes arquiteturas: x86, x86_64, ARM, ARM64, MIPS, 8086;
- Plugin para integração com a ferramenta IDAPro;
- Emulação de arquivos em um ambiente isolado ou em sandbox;
- Permite acesso à memória, registros e chamadas API ou syscalls;
- Permite modificar a memória ou os registros em tempo de execução;
- Permite a depuração de um arquivo usando a ferramenta GDB;
- Permite a emulação de código de shellcode.
NOTA: Este emulador não se destina a executar os arquivos rapidamente, mas se destaca pela possibilidade de poder analisá-los enquanto eles estão funcionando.
Finalmente, ao emular um arquivo binário, temos que fornecer ao emulador uma pasta chamada "rootfs", que conterá diferentes bibliotecas ou drivers de acordo com o sistema operacional ao qual o arquivo pertence para poder funcionar. Mais tarde, veremos um exemplo de como utilizar esta estrutura.
Capacidades do QILING ao emular um binário
Manipulando o emulador
Agora que já mencionamos brevemente para que serve esta estrutura e como ela funciona, continuaremos a explicar algumas de todas as ações que ela proporciona ao emular um arquivo binário.
Quando executamos um arquivo binário, ele pode ser executado no todo ou em parte, dando-lhe um endereço inicial e um endereço final. Na captura de tela seguinte, você pode ver como é executada uma execução parcial de um arquivo binário.
NOTA: Os endereços de memória podem variar de acordo com o tipo de arquivo que queremos executar.
Por outro lado, podemos configurar o nível de detalhe (verbose) com o qual queremos ver as informações geradas pelo emulador enquanto ele está funcionando, e salvá-las em um arquivo de registro ou simplesmente visualizá-las através do console.
Os níveis de detalhe que a estrutura permite podem ser:
- OFF - Exibe apenas mensagens de advertência;
- DEFAULT - Usado por padrão, exibe chamadas API e erros;
- DEBUG - Exibe mais informações do que "DEFAULT"; por exemplo, os endereços onde as DLLs são carregadas;
- DISASM - Exibe todas as instruções executadas;
- DUMP - O modo mais detalhado, capaz de exibir os dumps de registro, entre outras coisas.
Outra característica interessante desta estrutura é a possibilidade de se enganchar em diferentes ações, como por exemplo:
- Código assembler do arquivo binário;
- Endereços de memória.
Desta forma, podemos dizer ao emulador que quando um determinado endereço de memória é atingido, para invocar e executar uma função que tenhamos desenvolvido. Por outro lado, podemos também dizer a ele para executar uma função para cada linha de código assembler que é executada pelo emulador.
Nas imagens a seguir, você pode ver exemplos de como é possível enganchar as ações mencionadas acima.
Para os casos específicos de chamadas API ou syscalls, QILING permite fazer um hijack dessas chamadas em momentos diferentes; ou seja, antes, durante ou após a execução da chamada em questão. Desta forma, poderíamos modificar os atributos a serem utilizados pela chamada API ou modificar o código da API para algo desenvolvido por nós.
Na captura de tela seguinte, você pode ver como é possível realizar um hijack de algumas APIs do Windows.
Finalmente, mencionaremos outras ações que poderiam ser realizadas com este este framework:
- Manipulação da stack:
- Carregar um valor: ql.stack_push(VALOR)
- Ler: ql.stack_read(OFFSET)
- Manipulação de registros:
- Leitura: ql.reg.read(REGISTRO)
- Escrita: ql.reg.write(REGISTRO, VALOR)
- Manipulação da memória:
- Leitura: ql.mem.read(DIRECCION, TAMAÑO)
- Escrita: ql.mem.write(DIRECCION, VALOR)
- Busca de bytes: ql.mem.search(BYTES)
Exemplo
Abaixo, compartilhamos um exemplo de um caso em que o QILING pode ser útil para análise de malware.
Para isso, usaremos uma amostra do Conti ransomware com o hash 8FBC27B26C4C4C4C582B5764EACF897A89FE74C0A88D, para o sistema operacional Windows.
Esta amostra tem diferentes strings que o malware usa como “blacklist”, para detectar aquelas pastas cujo conteúdo não será criptografado. Por exemplo, ele não criptografará o conteúdo da pasta "Windows".
Como estas strings são criptografadas dentro da amostra, nosso objetivo é emular a rotina de descodificação para obter os nomes destas pastas.
A imagem a seguir mostra um exemplo do carregamento de bytes que representam uma string criptografada e a chamada para uma função encarregada de decodificá-los.
Para emular esta lógica, precisamos primeiro saber o endereço de memória onde a função encarregada de chamar a função de decodificação começa (este seria o endereço de entrada para o emulador). Neste caso, o endereço é 0x411AF0. Por outro lado, precisamos de um endereço para dizer ao emulador onde ele deve terminar. Olhando a Imagem 7, vemos que podemos usar o endereço 0x411B79.
Finalmente, como o resultado da função de decodificação é armazenado no registro EAX e então esse valor é carregado na memória, vamos enganchar o endereço de memória 0x411B73, que está encarregado de executar essa instrução.
Tendo obtido estes endereços de memória, procedemos a escrever um script que emula o malware usando estes endereços e exibe o resultado na tela.
A imagem a seguir mostra como seria o código do script.
Embora o script mostrado na Imagem 9 seja usado para decodificar uma determinada cadeia, esta amostra contém outras cadeias que usam uma lógica similar à vista na Imagem 7.
Portanto, se pararmos para olhar o código na imagem, vemos que após chamar a função que decodifica a string, o resultado é carregado na memória executando a instrução "mov [ebp+var_158],eax". Se olharmos a Imagem 11, vemos que esta instrução é repetida após cada chamada para as funções que descodificam as strings, modificando o offset onde o resultado tem que ser armazenado para não interferir com outras.
Portanto, podemos modificar o script na Imagem 9 para emular o malware até que ele termine de carregar todas as strings decodificadas, e em vez de nos prender a um endereço de memória, desta vez leremos cada instrução que for executada pelo malware e quando virmos algo como "mov [ebp+var_158],eax" imprimimos o valor para a tela.
Como podemos ver na imagem anterior, obtivemos todas as strings que representam as pastas cujo conteúdo não será criptografado por esta amostra Conti.
Por outro lado, é importante lembrar que esta amostra é para um sistema operacional Windows e a emulação foi realizada em um sistema operacional Linux.
Conclusão
QILING é um framework muito poderoso e versátil que nos dá a oportunidade de executar diferentes binários ou shellcodes independentemente do sistema operacional que utilizamos.
Devido à possibilidade de fazer diferentes tipos de modificações dinamicamente, tais como remendar um arquivo binário enquanto ele está sendo emulado, ou o fato de poder modificar chamadas para a API do Windows, entre outras coisas, é muito prático e interessante usar QILING para ver o comportamento do binário, seja ele um malware ou outros.
Por outro lado, ter a possibilidade de emular uma seção de um binário por meio de uma execução parcial é muito prático para casos em que se está analisando uma amostra de malware e se deseja apenas ter um melhor conhecimento de uma funcionalidade específica sem recorrer à execução completa da amostra. Ou mesmo para automatizar a análise dependendo de quanto conhecimento você tem sobre um determinado tipo de malware.
Veja mais: