Indirect Syscalls

Percebi que já havia abordado o tema de syscalls indiretos em outros posts, mas nunca tinha feito um post específico sobre o assunto. Então, neste post, vamos criar um loader que utiliza syscalls indiretos.


Entendendo o processo

Vou deixar abaixo algumas imagens que encontrei e que acredito serem úteis para entender melhor o assunto.

A figura abaixo mostra como funcionam as chamadas de sistema usando o exemplo do Bloco de Notas (notepad.exe). Quando o Bloco de Notas salva um arquivo, ele segue os seguintes passos:

  1. Primeiro, ele acessa o arquivo kernel32.dll e chama a função do Windows chamada WriteFile.

  2. Em seguida, o kernel32.dll chama outro arquivo chamado Kernelbase.dll para continuar o processo.

  3. Depois, a função WriteFile usa a função nativa do Windows chamada NtCreateFile, encontrada no Ntdll.dll. Essa função nativa tem as instruções para iniciar a "chamada de sistema", que é um comando que faz o computador trocar do modo usuário (onde os programas normais rodam) para o modo kernel (onde as partes mais importantes do sistema operam), e assim salva o arquivo.

Esses passos fazem com que o computador realize a transição de modos e consiga salvar o arquivo no disco.

A figura abaixo explica como funciona um AV/EDR ao monitorar e interceptar chamadas de sistema.

  1. Quando o Bloco de Notas quer criar um arquivo, ele chama a função CreateFileW usando a Kernel32.dll.

  2. Essa função passa para a Kernelbase.dll, que continua o processo normalmente.

  3. Antes da chamada de sistema ser realizada, o EDR interfere. Ele usa o arquivo Hooking.dll para modificar a função nativa NtCreateFile, que está dentro do Ntdll.dll. Isso é conhecido como "API Hooking".

  4. Depois que o EDR processa ou verifica a função, a chamada de sistema é finalmente executada.

  5. A função NtCreateFile continua, e o sistema realiza a transição para o modo kernel (Ring 0), onde a função é executada no nível mais baixo do sistema.

Com isso, o EDR consegue monitorar e até bloquear ações suspeitas antes que o sistema as execute.

A figura abaixo mostra a transição do modo de usuário para o modo kernel no contexto da execução de malware com chamadas de sistema diretas implementadas.

  1. O malware Malware.exe deseja realizar uma operação, como criar um arquivo, mas em vez de usar as APIs comuns do Windows, como CreateFileW(), ele opta por um método mais furtivo.

  2. Em vez de invocar a função NtCreateFile() através da Ntdll.dll (que é comumente usada para essas operações), o malware faz uso de "direct syscalls" (chamadas de sistema diretas). Ou seja, ele salta completamente as camadas intermediárias e invoca diretamente as instruções de syscall do sistema operacional, ignorando funções como NtCreateFile().

  3. Esse método de "direct syscalls" permite ao malware evitar interceptações ou modificações feitas por sistemas de monitoramento, como EDRs, que frequentemente "hookam" ou monitoram APIs de nível superior como Ntdll.dll.

  4. Ao fazer a chamada de sistema direta, a execução imediatamente transita para o modo kernel (Ring 0), onde a função de sistema KiSystemCall64 é chamada.

  5. O KiSystemCall64 pesquisa a tabela de descritores de serviço do sistema (SSDT) para encontrar o código da função correspondente, como NtCreateFile() ou a função de sistema diretamente referenciada.

  6. Finalmente, o sistema executa a operação no modo kernel com privilégios elevados, permitindo ao malware realizar sua ação sem ser detectado pelas ferramentas de segurança que monitoram as camadas superiores.

O uso de "direct syscalls" permite que o malware contorne facilmente as camadas de defesa baseadas em APIs monitoradas, evitando a maioria das técnicas de detecção que dependem do hook nas funções intermediárias.


Indirect syscalls

A figura abaixo ilustra como um malware utiliza a técnica de syscall indireta (indirect syscall) para realizar chamadas de sistema de maneira mais furtiva em comparação com a técnica de syscall direta (direct syscall).

  1. O malware Malware.exe prepara os registradores necessários para realizar a operação de forma semelhante à syscall direta. No entanto, em vez de fazer a chamada diretamente para o kernel, ele faz o salto para a instrução de syscall que já está dentro da Ntdll.dll.

    • Por que é menos suspeito?: Como a instrução syscall é executada na memória legítima da Ntdll.dll, ela parece uma operação legítima para o AV/EDR, já que a Ntdll.dll é uma parte confiável do sistema. Essa abordagem reduz as chances de detecção.

  2. Uma grande vantagem dessa técnica é que tanto a execução da syscall quanto a instrução de retorno (syscall return) ocorrem na memória da Ntdll.dll. Isso dá uma aparência de comportamento legítimo.

    • Evasão de AV/EDR: O EDR pode estar monitorando chamadas diretas de syscalls customizadas que executam operações maliciosas. No entanto, como a execução ocorre dentro de uma biblioteca de sistema legítima, como a Ntdll.dll, a execução é vista como "normal", dificultando a detecção.

  3. Quando a syscall é invocada a partir da Ntdll.dll, a transição para o modo kernel (Ring 0) ocorre normalmente, com a função KiSystemCall64 sendo executada, e a tabela SSDT (System Service Descriptor Table) consultada.

  4. Após a execução do comando syscall, a instrução de retorno (syscall return) redireciona o controle para a memória legítima da Ntdll.dll, e, a partir daí, o fluxo de execução retorna ao malware.

    • Diferença com Direct Syscalls: Na técnica de direct syscall, o malware executa diretamente a instrução syscall, o que pode levantar suspeitas, pois a execução ocorre em uma região de memória fora de uma biblioteca legítima. Isso pode ser detectado mais facilmente por ferramentas de segurança.

  5. A técnica de syscall indireta é, portanto, uma evolução da syscall direta, pois resolve problemas de evasão de AV/EDR, tornando a atividade maliciosa menos detectável. Ao executar tanto a syscall quanto o retorno dentro da Ntdll.dll, o malware se mistura melhor com as operações legítimas do sistema, enganando as defesas baseadas em comportamento.

Essa técnica torna o malware significativamente mais furtivo, pois explora o fato de que os AV/EDRs confiam no código da Ntdll.dll e não "esperam" que a execução maliciosa esteja ocorrendo a partir desse local confiável.


Código

Aqui está a nossa func.h, que define algumas funções essenciais para a execução de syscalls indiretas. Nela, incluímos a estrutura CLIENT_ID, que ajuda a identificar processos e threads, e OBJECT_ATTRIBUTES, que armazena atributos de objetos do Windows. Também declaramos funções como NtOpenProcess, NtAllocateVirtualMemory, e outras, que serão usadas para interagir com processos e memória de forma direta.


Agora em nosso código main

Começamos incluindo nosso cabeçalho func.h, que reúne as declarações necessárias para as funções NT que utilizamos.

Utilizamos o GetProcessIdByName para buscar o PID (Process ID) de um processo alvo pelo seu nome.

Além disso, resolvemos ponteiros de função para chamadas de API nativas do Windows, extraídas de ntdll.dll, obtendo os números de syscalls e os endereços dessas syscalls para funções como NtOpenProcess, NtAllocateVirtualMemory, e outras. Nosso objetivo final é abrir o processo de destino, alocar memória, escrever o shellcode nessa memória alocada e executar o shellcode, usando syscalls.


Ofuscação de Nomes de Funções

Como mencionamos anteriormente, a técnica de ofuscação utilizada para os nomes das funções NT é interessante:

Ao definir os nomes como arrays de caracteres em vez de strings, estamos criando uma barreira contra análise de código estático. Isso pode ser uma abordagem importante em alguns cenários, pois torna mais "difícil" para ferramentas de detecção identificarem facilmente as operações que o código realiza.


Obtenção de Endereços de Funções

Fazemos o uso de GetProcAddress em conjunto com GetModuleHandleA para recuperar os endereços das funções NT. Também temos um deslocamento de 4 bytes adicionado ao endereço da função recuperada, seguido pela soma de 0x12 ao endereço recuperado. Exemplo:

NtOpenProcess = 0x00007FF98C5ADA10 <-- Endereço

NtOpenProcess Syscall = 0x00007FF98C5ADA22 <-- Endereço

0x00007FF98C5ADA22 - 0x00007FF98C5ADA10 = 0x12

0x00007FF98C5ADA10 + 0x12 = 0x00007FF98C5ADA22


Manipulação de Memória

Temos nossas chamadas NtAllocateVirtualMemory e NtWriteVirtualMemory para alocar espaço de memória no processo alvo e escrever o shellcode. A alocação de memória em um processo remoto exige permissões adequadas. O uso de MEM_COMMIT | MEM_RESERVE em NtAllocateVirtualMemory é importante, pois garante que a memória alocada esteja disponível e pronta para uso.


Criação de Threads

Realizamos a criação de uma thread remota com NtCreateThreadEx. A função inicia a execução do Shellcode escrito na memória, fazemos uso de NtWaitForSingleObject para esperar a conclusão da thread que inciamos.



Agora nosso código asm

Extern Indica que o símbolo é definido em outro módulo. SSNtOpenProcess:DWORD É o Número de syscall para NtOpenProcess. Obtemos o número SSN de uma possível função NTAPI lendo o valor no deslocamento 0x4 no stub de montagem da referida função:

AddrNtOpenProcess É o endereço real da instrução syscall de NtOpenProcess em ntdll.dll. Obtemos o endereço da instrução syscall adicionando 0x12 ao endereço de pNtOpenProcess

jmp QWORD PTR [AddrNtOpenProcess] É um salto incondicional, ela vai pular para o endereço AddrNtOpenProcess que vai ser o endereço da instrução syscall de NtOpenProcess em ntdll.dll.


Prova de conceito:

Note que o primeiro executável que testamos é um loader que utiliza APIs NT mas não faz uso de syscalls indiretas. Já o segundo executável é o loader que, de fato, faz uso de syscalls indiretas.


Detecção

Obtive 5 detecções no VirusTotal. Não está muito bom, mas dá para melhorar:


Contra Windows Defender

O Windows Defender não foi grande coisa, conseguimos contorná-lo facilmente. Apenas apliquei descriptografia RC4, que já foi abordada em um post anterior, e fiz uso do Havoc. Lembrando que este código ainda tem muito espaço para melhorar.


Contra Sophos EDR

Bom, eu fiz esse post há cerca de uma semana, e como estou livre, sem nada para fazer, decidi realizar mais um teste com o código de indirect syscalls. Decidi ver como ele se sairia contra o SOPHOS. De início, percebi que, sem realizar o unhooking da ntdll, não seria possível nem passar da parte de alocação de memória. Então, decidi utilizar um código simples para realizar o unhooking da ntdll.dll e verificar se conseguiria prosseguir com sua execução normalmente. E este foi o resultado:

Last updated