Definição do carregador refletivo Cobalt Strike

Homem vestindo um suéter preto usando um notebook na rua representando um hacker

Embora os componentes de IA e aprendizado de máquina de última geração das soluções de segurança continuem aprimorando os recursos de detecção baseada em comportamento, em sua essência, muitos ainda dependem de detecções baseadas em assinatura. O Cobalt Strike sendo um framework popular de comando e controle (C2) de equipe vermelha, usado tanto por agentes de ameaça quanto por equipes vermelhas desde sua estreia, continua a ser fortemente assinado por soluções de segurança.

Para continuar o uso operacional do Cobalt Strike no passado, nós, da IBM X-Force Red Adversary Simulation, investimos esforços significativos de pesquisa e desenvolvimento para personalizar o Cobalt Strike com ferramentas internas. Algumas de nossas ferramentas internas específicas para o Cobalt Strike possuem versões públicas, como “InlineExecute-Assembly”, “CredBandit” e “BokuLoader”. Nos últimos dois anos, dada a assinatura excessiva do Cobalt Strike, restringimos seu uso à simulação de agentes da ameaça menos sofisticados e, em vez disso, aproveitamos outros C2 de terceiros e internos ao realizar exercícios mais avançados de red team.

Por meio de esforços de pesquisa e desenvolvimento, encontramos o melhor sucesso operacional em exercícios avançados de equipe vermelha com:

  • Ferramentas internas personalizadas.
  • Carregadores internos personalizados.
  • Framework de C2 interno personalizado.
  • Continuar investindo na expansão dos recursos e da furtividade de frameworks de C2 alternativos de terceiros.

No entanto, ainda há uma grande quantidade de agentes de ameaças utilizando cópias piratas do Cobalt Attack, e continua sendo importante poder simular esses agentes de ameaças. Para as equipes vermelhas dispostas a se esforçar em pesquisa e desenvolvimento, elas ainda podem encontrar sucesso operacional com o Cobalt Attack ao simular esses adversários. Além disso, o Cobalt Strike é uma ótima ferramenta de aprendizado, que pode ser aproveitado por recém-chegados para obter experiência com um framework de C2 por meio de cursos de treinamento de equipe vermelha.

À medida que continuamos a expandir nossos recursos de C2, estamos compartilhando alguns insights sobre como, no passado, evoluímos a partir do framework Cobalt Strike, especificamente por meio do desenvolvimento de carregadores reflexivos personalizados. Este conteúdo também tem como objetivo ajudar defensores a compreender melhor como o Cobalt Strike funciona, de modo a possibilitar a criação de detecções mais robustas.

Expandindo o framework com carregadores refletivos

Este post de blog é o primeiro de uma série que serve como um guia introdutório, abordando os fundamentos do desenvolvimento de um carregador refletivo para o Cobalt Strike. À medida que avançarmos nesta série, iremos construir sobre essa base e fazer referência a este post ao longo do caminho.

Ao final desta série, o nosso objetivo é criar um carregador refletivo que se integre aos mecanismos de evasão já existentes do Cobalt Strike e que, inclusive, os amplie com técnicas avançadas que atualmente não estão presentes na ferramenta. Os próximos artigos irão aprofundar-se no desenvolvimento de funcionalidades específicas de evasão e em como implementá-las no nosso carregador refletivo do Cobalt Strike.

Para dar início, este artigo abordará:

  • Os problemas de carregar um implante de C2 a partir do disco usando o carregador de DLL do Windows.
  • Os conceitos e o funcionamento do processo de carregamento refletivo do Cobalt Strike.
  • Os requisitos de design necessários para construir um carregador refletivo eficaz.
  • As fases envolvidas no processo de carregador refletivo.

À medida que exploramos o reflective loading do Cobalt Strike sob a perspectiva de um desenvolvedor de ferramentas de segurança ofensiva, iremos destacar oportunidades tanto para detecção quanto para evasão. Alguns aspectos do desenvolvimento serão omitidos ou simplificados, e encorajamos o leitor a preencher essas lacunas depurando projetos existentes de carregador refletivo, reconstruindo-os do zero ou buscando formação especializada.

Carregamento da DLL do Beacon

O implante de C2 do Cobalt Strike, conhecido como Beacon, é uma biblioteca de vínculo dinâmico do Windows (Dynamic-Link Library – DLL). O recurso modular de utilizar o nosso próprio carregador de DLL no Cobalt Strike é conhecida como User-Defined Reflective Loader (UDRL).

O carregador de DLL nativo do Windows

Normalmente, o carregador de DLL nativo do Windows é responsável por carregar DLLs no espaço de memória virtual de um processo. Esse carregador existe principalmente em user space, embora atravesse para o kernel quando mapeia DLLs a partir do disco.

O uso do carregador de DLL do Windows apresenta algumas desvantagens quando empregado em simulações de adversários:

  • A DLL em formato bruto precisa estar presente no sistema de arquivos.
  • A DLL em formato bruto precisa estar livre de ofuscação.
  • Eventos de carregamento de imagem no kernel são acionados pelo carregador de DLL do Windows.

Por esses motivos, utilizar o carregador de DLL do Windows para carregar a DLL do Beacon não é uma solução ideal. Para contornar essas limitações, carregamos a DLL do Beacon diretamente da memória por meio de um carregador reflexivo.

Os três principais pontos de detecção que o carregamento reflexivo evita são:

  1. Evita malware com assinatura presente no sistema de arquivos.
  2. Evita eventos de carregamento de imagem no kernel, que podem ser monitorados por soluções de segurança.
  3. Evita que a DLL do implante de C2 apareça listada no Process Environment Block (PEB).

Carregador reflexivo vs. carregador de DLL do Windows

O carregamento reflexivo pode ser entendido, de forma simples, como o carregamento de uma DLL em formato bruto diretamente da memória, em vez de a partir do sistema de arquivos.

Tanto o carregamento reflexivo quanto o carregador de DLL do Windows têm o mesmo objetivo: carregar uma DLL do formato bruto de arquivo para o espaço de memória virtual de um processo. No entanto, o carregamento reflexivo apresenta uma vantagem fundamental em relação ao carregador de DLL do Windows: ele não exige que o arquivo da DLL exista no sistema de arquivos. Esse carregamento inteiramente em memória permite um número praticamente ilimitado de fases de carregamento em cadeia, já que a DLL do implante de C2 pode ficar oculta sob múltiplas camadas de criptografia e codificação dentro da memória do processo.

Formato bruto de arquivo vs. formato de endereço virtual

Um conceito essencial ao carregar uma DLL é compreender que ela possui formatações diferentes quando está em disco e quando está em memória. As principais diferenças entre a DLL em formato bruto de arquivo e em formato de endereço virtual são:

Formato de arquivo bruto:

  • É o formato da DLL tal como ela existe no sistema de arquivos.
  • As seções da DLL ficam compactadas umas junto às outras.
  • Os deslocamentos são baseados no início do arquivo bruto da DLL, conforme ele existe em disco.
  • Esse formato ocupa menos espaço em memória.

Formato de endereço virtual:

  • É o formato da DLL quando ela está carregada no espaço de memória virtual de um processo.
  • As seções ficam espaçadas entre si.
  • Os deslocamentos são Relative Virtual Addresses (RVA).
  • Quando em execução dentro de um processo, a DLL e os demais módulos determinam localizações com base em RVAs.
  • Esse formato ocupa mais espaço em memória.

Beacon bruto vs. beacon virtual

Ao analisar uma DLL de beacon HTTP na ferramenta PE-Bear, de Aleksandra Doniec, é possível observar claramente as diferenças entre o endereçamento bruto e o endereçamento virtual de cada seção da DLL.

Tabela listando os endereços brutos e virtuais de cada seção da DLL do Beacon.

Tabela listando os endereços brutos e virtuais de cada seção da DLL do Beacon.

Essa DLL de beacon HTTP/S possui 0x52000  bytes (327KB ) quando carregada no espaço de memória virtual de um processo, em comparação com 0x44000  bytes (272KB ) quando existe no sistema de arquivos. Essa diferença de tamanho ocorre porque, no formato de endereço virtual, as seções ficam espaçadas entre si, enquanto no formato bruto de arquivo elas permanecem compactadas.

O PE-Bear fornece uma representação visual da DLL do Beacon tanto no formato bruto de arquivo quanto no formato de espaço de endereços virtuais.

Representação visual da LLC do beacon em formato bruto versus formato virtual

Representação visual da LLC do beacon em formato bruto (à esquerda) versus formato virtual (à direita)

Carregamento do Beacon com o carregador de DLL do Windows

Embora não seja a abordagem mais prudente durante uma simulação de adversário, gravar uma DLL de beacon em formato bruto no disco, sem qualquer ofuscação, e carregá-la usando o carregador de DLL do Windows é uma excelente forma de desmistificar tanto o funcionamento do beacon quanto o processo de carregamento de DLLs. Em essência, o beacon é apenas uma DLL. Tanto o carregador de DLL do Windows quanto um carregador reflexivo têm a mesma função básica: carregar uma DLL dentro de um processo.

Para carregar a DLL do beacon utilizando o carregador de DLL do Windows, seguimos os passos abaixo:

  1. Gerar uma DLL de beacon em formato bruto, sem ofuscação.
  2. Criar um programa que:
    1. Utilize a API LoadLibrary para carregar a DLL do beacon a partir do disco.
    2. Execute o beacon chamando o ponto de entrada da DLL do beacon em formato virtual.
  3. Colocar o executável do programa e a DLL do beacon na mesma pasta.
  4. Executar o programa.

Gerando uma DLL de beacon em formato bruto, livre de ofuscação

Primeiro, desativamos todas as opções de Malleable PE que tornam a DLL do beacon incompatível com o carregador de DLL do Windows. Para isso, modificamos o nosso perfil Malleable C2 e desabilitamos as opções de evasão do Malleable PE localizadas no bloco de estágio.

Bloco de estágio do perfil Malleable C2 modificado para desabilitar os recursos de evasão do Cobalt Strike.

Bloco de estágio do perfil Malleable C2 modificado para desabilitar os recursos de evasão do Cobalt Strike.

Após modificar o perfil, reiniciamos o Cobalt Strike Team Server, fornecendo o nosso perfil no_evasion.profile  como argumento.

Captura de tela feita para a postagem do blog

Nós nos conectamos ao Team Server com o cliente Cobalt Strike. Em seguida, criamos umWindows Stageless Payload  com a opção de saída definida como Raw e o listener definido comohttps . Salvamos a carga útil como beacon.dll .

Captura de tela da criação de uma DLL de beacon “raw stageless” a partir do cliente Cobalt Strike

Captura de tela da criação de uma DLL de beacon “raw stageless” a partir do cliente Cobalt Strike

Criando nosso programa carregador de DLL de Beacon

Usando o código abaixo, criamos um programa em C chamado loadBeaconDLL.c  e compilamos:

Código C para Windows para carregar a DLL do beacon a partir do disco usando o carregador de DLL do Windows.

Código C para Windows para carregar a DLL do beacon a partir do disco usando o carregador de DLL do Windows.

Usamos a API Kernel32.LoadLibraryA  para carregar nossa DLL de beacon raw a partir do disco. Essa API chamará o carregador de DLL do Windows integrado, que carregará nossa DLL de beacon do disco para o espaço de memória virtual do nosso processo de host.

Como parte do processo de carregamento, o carregador de DLL do Windows inicializará nossa DLL de beacon chamando seu ponto de entrada com DLL_PROCESS_ATTACH (1)  como argumento.

Depois que o carregador de DLL do Windows carregar e inicializar nossa DLL de beacon no espaço de memória virtual do nosso processo, precisaremos chamar novamente o ponto de entrada da DLL de beacon virtual com o argumento 0x4.

O nosso programa precisa conhecer o ponto de entrada da DLL do beacon virtual para poder executá-la. Isso pode ser feito dinamicamente no próprio programa, analisando os cabeçalhos da DLL virtual do beacon para obter o Relative Virtual Address (RVA) do ponto de entrada, ou, de forma mais simples, podemos identificar esse valor previamente e codificá-lo diretamente no programa.

Para esta prova de conceito, iremos descobrir manualmente e codificar diretamente no programa o RVA do ponto de entrada da DLL do beacon. Usando o PE-Bear, descobrimos que o RVA do ponto de entrada do beacon é 0x1D840 :

Captura de tela da identificação do RVA do ponto de entrada da DLL do beacon usando o PE-Bear

Captura de tela da identificação do RVA do ponto de entrada da DLL do beacon usando o PE-Bear

O método LoadLibraryA A API  retorna o endereço base da nossa DLL de beacon virtual. Basta somar esse endereço base ao RVA do ponto de entrada para determinar o endereço final do ponto de entrada.

Com o código pronto, compilamos o nosso programa em C em um executável para Windows:

Comando usado para compilar o nosso programa.

Comando usado para compilar o nosso programa.

Posicionando o programa e a DLL do Beacon no sistema de arquivos

Ao colocar a DLL do beacon e o nosso programa carregador do beacon no mesmo diretório, o carregador de DLL do Windows consegue localizar a DLL automaticamente durante o processo de carregamento.

Colocamos tanto beacon.dll  e loadBeaconDLL.exe quanto no sistema de arquivos, dentro do mesmo diretório:

DLL do Beacon e programa carregador posicionados no mesmo diretório.

DLL do Beacon e programa carregador posicionados no mesmo diretório.

Executando o nosso programa

A partir do desktop do Windows, damos um duplo clique no programa loadBeaconDLL.exe e estabelecemos uma conexão ativa do beacon com o Team Server.

Conexão bem-sucedida com o Team Server de C2 a partir da DLL do beacon carregada usando o carregador de DLL do Windows.

Conexão bem-sucedida com o Team Server de C2 a partir da DLL do beacon carregada usando o carregador de DLL do Windows.

Carregamento reflexivo do Cobalt Strike

O Cobalt Strike usa uma versão modificada do projeto do Carregador Refletivo, de Stephen Fewer. Esse lendário carregador de DLL em memória existe há mais de uma década e já foi utilizado no Metasploit e em outras ferramentas notáveis de segurança ofensiva.

Considerações sobre o uso do UDRL

Ao longo dos anos, o carregador reflexivo do Cobalt Strike foi aprimorado para lidar com todos os recursos de evasão do Malleable PE que a ferramenta oferece. A principal desvantagem de utilizar um User-Defined Reflective Loader (UDRL) personalizado é que os recursos de evasão do Malleable PE podem ou não ser suportados nativamente, exigindo implementação adicional.

Este lendário carregador de DNS na memória tem mais de uma década de idade e tem sido usado no Metasploit e em outras ferramentas de segurança ofensiva notáveis. No entanto, atualmente, funcionalidades como obfuscate precisam ser tratadas pelo próprio UDRL, enquanto outras, como sleepmask e cleanup podem ser gerenciadas pelo beacon quando há uma integração adequada com o UDRL.

Métodos de carregamento reflexivo

Método original do carregador reflexivo

O projeto original de Carregador Refletivo exige que o ReflectiveLoader  seja compilado diretamente no projeto da DLL e exportado dentro da DLL do implante de C2.

Em seguida, outro projeto fica responsável por:

  1. Descobrir o endereço virtual da exportação ReflectiveLoader  .
  2. Executar a exportação ReflectiveLoader  , que retorna o ponto de entrada da DLL carregada.
  3. Chamar o ponto de entrada da DLL carregada por carregamento reflexivo.
Diagrama do carregador refletivo original, carregando uma LLC na memória virtual.

Diagrama do carregador refletivo original, carregando uma LLC na memória virtual.

Anexar método de carregador refletivo

Um método alternativo é anexar o carregador refletivo à LLC. Isso permite que qualquer vulnerabilidade não gerenciada seja carregada e não requer a compilação da vulnerabilidade a partir do código-fonte. Esse é um método robusto de carregamento refletivo capaz de carregar qualquer arquivo PE (EXE ou LLC).

Diagrama de um carregador refletivo anexado a uma LLC, carregando uma LLC na memória virtual.

Diagrama de um carregador refletivo anexado a uma LLC, carregando uma LLC na memória virtual.

Método de carregador refletivo da Cobalt Strike

A implementação de cargas refletivas da Cobalt Strike usa um híbrido dos dois métodos acima. Este método de carregamento refletivo pode ser familiar para aqueles que já sabem como o Meterpreter da Metasploit faz carregamento refletivo.

Assim como no método original de carregamento reflexivo, a função ReflectiveLoader  é compilada e exportada dentro da DLL original do beacon. Quando um operador gera um payload de beacon a partir do cliente do Cobalt Strike, o mecanismo Malleable PE do Cobalt Strike aplica patches na DLL bruta do beacon para informar ao carregador reflexivo quais opções do Malleable PE devem ser utilizadas. O cabeçalho DOS do beacon é modificado para chamar a exportação ReflectiveLoader em um deslocamento codificado de forma fixa. Os bytes iniciais do cabeçalho DOS do beacon que realizam essa chamada à exportaçãoReflectiveLoader  serão referidos neste blog como o “call reflective loader stub”.

Quando um UDRL é carregado no Cobalt Strike e um operador gera um payload de beacon a partir do cliente, o mecanismo Malleable PE do Cobalt Strike injeta o shellcode do carregador reflexivo no deslocamento do arquivo bruto correspondente à exportação ReflectiveLoader  .

Após o mecanismo Malleable PE concluir a aplicação dos patches na DLL bruta do beacon, essa DLL é entregue ao operador em um formato executável semelhante a um shellcode.

Diagrama do carregador reflexivo do Cobalt Strike carregando a DLL do beacon para a memória virtual.

Diagrama do carregador reflexivo do Cobalt Strike carregando a DLL do beacon para a memória virtual.

Stub de carregador refletivo de chamada do Beacon

Ao observar os bytes iniciais no desmontador PE-Bear, podemos ver que a própria DLL do beacon é executável.

O stub do carregador refletivo de chamada mostrado como códigos de operação em assembly executáveis.

O stub do carregador refletivo de chamada mostrado como códigos de operação em assembly executáveis.

Os bytes iniciais MZAR  são personalizáveis por meio das opções do Malleable PE no perfil de C2 do Cobalt Strikes. Esses bytes precisam ser executáveis e resultar em uma operação de não operação (nop ).

Após a execução de nops  opcionais e de magic bytes, o stub de carregador refletivo de chamada:

  • Cria um quadro de stack.
  • Usa endereçamento relativo a RIP para determinar o endereço base da DLL do beacon em formato bruto.
  • Chama a exportação ReflectiveLoader  no deslocamento conhecido 0x16E3C  do arquivo bruto.
  • Chama o ponto de entrada da DLL do beacon já carregada.

Confirmamos que o deslocamento no arquivo bruto da exportação ReflectiveLoader  é 0x16E3C  ao analisar o diretório de exportações da DLL do beacon.

Captura de tela do uso do PE-Bear para determinar o deslocamento do arquivo bruto da exportação do ReflectiveLoader.

Captura de tela do uso do PE-Bear para determinar o deslocamento do arquivo bruto da exportação do ReflectiveLoader.

Como aparece no diretório de exportações, o endereço da exportação ReflectiveLoader está no formato RVA, referindo-se à DLL do beacon já em seu estado virtual. Como a exportação ReflectiveLoader  é executável, sabemos que ela reside na seção.text da DLL do beacon.

Para identificar o deslocamento correspondente no arquivo bruto da exportação ReflectiveLoader  , é necessário compreender a diferença entre os endereços virtual e bruto da seção .text  . Com a diferença conhecida, podemos simplesmente subtraí-la do RVA da exportação ReflectiveLoader  para descobrir o deslocamento no arquivo bruto da exportação ReflectiveLoader  .

Os endereços virtual e bruto da seção .text  estão listados nos cabeçalhos de seção da DLL do beacon:

Endereços bruto e virtual da seção .text da DLL do beacon.

Endereços bruto e virtual da seção .text da DLL do beacon.

A diferença entre os dois é de 0xC00  bytes. Ao subtrair o RVA da exportação ReflectiveLoader  de 0x17A3C  pela diferença, descobrimos que o deslocamento do arquivo bruto é 0x16E3C .

Podemos confirmar isso no PE-Bear clicando com o botão direito do mouse no RVA da função da exportação ReflectiveLoader  e clicando em Follow RVA:17A3C . O visualizador hex no widget acima acessará a visualização da exportação do ReflectiveLoader  no seu deslocamento de arquivo bruto.

Em resumo, o fluxo do processo de carregamento refletivo do Cobalt Attack é:

  • Um thread executa a LLC do beacon bruto.
  • O stub do carregador refletivo da chamada invoca a expotação ReflectiveLoader  em um deslocamento de arquivo bruto conhecido.
  • O carregador refletivo carrega a LLC do beacon bruto na memória virtual do processo do hot.
  • Após o carregamento, o carregador refletivo retorna o ponto de entrada da LLC do beacon virtual para o stub do carregador refletivo de chamadas.
  • O stub do carregador refletivo da chamada invoca o ponto de entrada da LLC do beacon virtual.
Diagrama mostrando as fases principais de como o Cobalt Attack realiza o carregamento refletivo da LLC do beacon.

Diagrama mostrando as fases principais de como o Cobalt Attack realiza o carregamento refletivo da LLC do beacon.

Requisitos de design do carregador refletivo

Código independente de posição

Como nosso carregador refletivo é executado antes que a LLC do beacon seja carregada, o código do carregador refletivo precisa ser um shellcode puro.

A maneira mais fácil de criar um shellcode complexo é escrevê-lo em C sem dependências externas. Em seguida, o arquivo C é compilado em um arquivo-objeto. Tudo deve ser incluído na seçãotext do arquivo-objeto. Por fim, extraímos a seção.text para obter o shellcode do carregador refletivo.

Como o Cobalt Attack insere nosso UDRL

O mecanismo de Malleable PE do Cobalt Strike irá lidar com o trabalho de obter o shellcode do nosso arquivo de objeto de carregador refletivo e corrigi-lo na SSD do beacon bruto no deslocamento do arquivo bruto da exportação ReflectiveLoader  . Isso é feito no script UDRL Aggressor, conforme abaixo:

Script do Aggressor para escrever o shellcode de carregador refletivo na DLL do beacon bruto usando o Cobalt Strike.

Script do Aggressor para escrever o shellcode de carregador refletivo na DLL do beacon bruto usando o Cobalt Strike.

O nosso script UDRL Aggressor faz com que o Cobalt Strike escreva o shellcode do nosso carregador reflexivo executando as seguintes etapas:

  • Abrimos um$handle para o arquivo-objeto do nosso UDRL utilizando a funçãoopenf funções de negócios.
  • Com o arquivo$handle , lemos o fluxo de bytes e o armazenamos na variável de array de bytes$data .
  • Em seguida, fechamos arquivo$handle utilizando a funçãoclosef funções de negócios.
  • A função Aggressor nativa do Cobalt Strike
    <a href="https://hstechdocs.helpsystems.com/manuals/cobaltstrike/current/userguide/content/topics_aggressor-scripts/as-resources_functions.htm#extract_reflective_loader">extract_reflective_loader</a>
    
    analisará o arquivo objeto do nosso UDRL a partir da matriz de bytes$data , localizará a seção.text do nosso arquivo-objeto UDRL, extrairá a seção .text e a salvará na variável de matriz de bytes$loader .
  • A função Aggressor nativa do Cobalt Strike
    <a href="https://hstechdocs.helpsystems.com/manuals/cobaltstrike/current/userguide/content/topics_aggressor-scripts/as-resources_functions.htm#setup_reflective_loader">setup_reflective_loader</a>
    
    A função Aggressor do Cobalt Strike usará o mecanismo Malleable PE para descobrir o deslocamento do arquivo bruto da nossa exportaçãoReflectiveLoader e corrigir nosso shellcode de UDRL a partir do$loader .
  • Por fim, retornamos a DLL do beacon modificada para o Cobalt Strike e salvamos nosso arquivo do cliente.

Fases de carregamento reflexivo

O Cobalt Strike já fez o trabalho pesado para nós no que diz respeito a extrair a seção .text do arquivo objeto do nosso carregador reflexivo, aplicar o patch com o shellcode do nosso carregador reflexivo e chamar o nosso carregador reflexivo por meio do stub de carregador refletivo de chamada, localizado no cabeçalho da DLL do beacon.

Estas são as fases que precisamos desenvolver para carregar o beacon por carregamento reflexivo:

  1. Encontra a DLL do Beacon Bruto
  2. Analisar Cabeçalhos de DLL de Beacon
  3. Alocar Memória para DLL do Virtual Beacon
  4. Carregar Seções para o Espaço de Memória Virtual
  5. Carregar Dependências de DLL
  6. Resolver Tabela de Endereço de Importação
  7. Resolver Realocações
  8. Executar Beacon

Fase 1: Localização do endereço base da DLL do beacon bruto

Existem vários métodos diferentes que podemos usar para lidar com o endereço da DLL do beacon bruto na memória. Alguns métodos são:

  • Varredura reversa em busca dos cabeçalhos MZ e PE
  • Varredura reversa em busca de um egg
  • Obtenção do endereço base da DLL bruta do beacon a partir do stub chamador do carregador refletivo

Determinando nossa posição em memória

Ao utilizar técnicas baseadas em varredura reversa, o primeiro passo é obter o endereço atual do ponteiro de instrução (RIP ). Para isso, podemos empregar um truque simples chamado de getRip :

  1. No UDRL, criamos uma função denominada getRip .
  2. Chamamos a função getRip  , o que faz com que o endereço da instrução imediatamente seguinte à chamadacall getRip " seja automaticamente colocado no topo da a stack. Esse endereço corresponde ao endereço de retorno.
  3. Em seguida, dentro da própria função getRip  , simplesmente copiamos esse endereço de retorno do chamador diretamente do topo da stack.
  4. Na codificação C do Windows x64, as funções podem retornar um valor. Esse valor é retornado ao responsável pela chamada por meio do registro RAX  registro. Ao lidar com o endereço do remetente do chamador para o RAX  , estamos retornando ao chamador o endereço de retorno do próprio chamador.
Código em assembly Intel x64 para obter o endereço base da DLL de beacon raw a partir do registrador RDI.

Código em assembly Intel x64 para obter o endereço base da DLL de beacon raw a partir do registrador RDI.

Varredura regressiva dos cabeçalhos MZ e PE

O projeto original do carregador refletivo faz uma varredura regressiva em busca dos cabeçalhos MZ e PE. Esses cabeçalhos se tornaram pontos de detecção. Para contornar isso, o Cobalt Strike adicionou os recursos de evasão de Malleable PE magic_mz  e magic_pe  .

documentação do Cobalt Strike afirma que a opção magic_mz  :

  • “Substitui os primeiros bytes (incluindo o cabeçalho MZ) da DLL refletiva do Beacon. Instruções válidas são obrigatórias. Siga instruções que alterem o estado da CPU com instruções que desfaçam a alteração.”

Quando configurado, os bytes MZ--  no deslocamento de arquivo raw 0x00  e o PE00  e os bytes 0x80 no offset de arquivo raw  são conhecidos pelo carregador refletivo. Eles são corrigidos na DLL do beacon pelo mecanismo de Malleable PE.

Esses bytes precisam ser relativamente exclusivos, caso contrário o carregador refletivo não conseguirá encontrá-los. Além disso, os bytes do cabeçalho MZ devem ser de não-operação e executáveis. Eles não podem ser valores como 0x00  ou o beacon pode falhar. Isso pode ser um possível ponto de detecção.

Varredura regressiva por um “egg”

Após identificar esse possível ponto de detecção, desenvolvi um método diferente, porém semelhante, para encontrar o endereço base da DLL de beacon raw. Esse método utiliza um caçado de egg capaz de pesquisar de trás para frente a partir de RIP , realizando uma varredura regressiva para localizar duas instâncias repetidas de um “egg” exclusivo de 64 bits no deslocamento de arquivo raw conhecido beacon.dll+0x50  .

O endereço beacon.dll+0x50  foi escolhido porque corresponde ao local do banner “This program cannot be run in DOS mode”, que não é necessário quando o beacon é carregado de forma reflexiva.

Como não temos acesso fácil ao mecanismo Java Malleable PE, o script Aggressor do UDRL BokuLoader.cna  pode ser usado para escrever o “egg”0xB0C0ACDC  no beacon. O código abaixo mostra como a DLL raw do beacon pode ser modificada para conter o egg:

Script do Aggressor para escrever um egg na DLL do beacon raw e exibir as alterações no console de script do Cobalt Strike.

Script do Aggressor para escrever um egg na DLL do beacon raw e exibir as alterações no console de script do Cobalt Strike.

O código UDRL deve saber o valor do egg escrito na DLL do beacon raw pelo script UDRL. Com o egg conhecido, o caçador de egg procura por duas instâncias do egg, conforme visto no código abaixo:

Código de assembly intel x64 para um caçador de egg que procura de trás para frente duas instâncias de um egg de 64 bits.

Código de assembly intel x64 para um caçador de egg que procura de trás para frente duas instâncias de um egg de 64 bits.

  • Tanto o script do Aggressor UDRL quanto o código UDRL C podem ser modificados para usar eggs diferentes.

Agora que os cabeçalhos MZ e PE não são mais usados, podemos removê-los do script do Aggressor UDRL:

Script do Aggressor para mascarar MZ, PE e bytes não utilizados do banner do DOS localizados nos cabeçalhos da PHP do beacon raw.

Script do Aggressor para mascarar MZ, PE e bytes não utilizados do banner do DOS localizados nos cabeçalhos da PHP do beacon raw.

Obtendo o endereço base da DLL de Beacon raw a partir do stub do Carregador Refletivo de Chamadas

Existe também outra forma, específica do Cobalt Strike, de descobrir o endereço base da DLL de beacon raw. Como visto anteriormente, os bytes iniciais no stub do carregador refletivo de chamadas armazenam o endereço base da DLL de beacon raw no registrador RDI  antes de chamar o carregador refletivo.: Em vez de realizar uma varredura regressiva a partir de RIP em busca de algum “egg”, podemos simplesmente obter o valor diretamente do registradorRDI  no início do código do nosso carregador refletivo.

Para examinar isso mais a fundo no depurador, geramos um beacon, prefixamos um breakpoint (0xCC ) e abrimos o beacon no x64dbg. Como o breakpoint é prefixado, o endereço base do beacon raw fica em +1  da memória alocada. Como visto acima, o stub do carregador refletivo de chamadas usa endereçamento relativo aRIP para obter o endereço base da DLL de beacon raw:

Captura de tela do x64dbg mostrando o passo a passo pelo stub do carregador refletivo de chamadas, evidenciando que o endereço base da DLL de beacon raw é salvo no registrador RDI antes da chamada ao carregador refletivo.

Captura de tela do x64dbg mostrando o passo a passo pelo stub do carregador refletivo de chamadas, evidenciando que o endereço base da DLL de beacon raw é salvo no registrador RDI antes da chamada ao carregador refletivo.

Abaixo está um exemplo funcional de como obter o endereço base da DLL de beacon raw a partir do stub do carregador refletivo de chamadas:

Código C com assembly inline para obter o endereço base da DLL de beacon raw a partir do registrador RDI.

Código C com assembly inline para obter o endereço base da DLL de beacon raw a partir do registrador RDI.

Fase 2: Analisando os cabeçalhos da DLL de Beacon

Com o endereço base da DLL de beacon raw, agora podemos obter os valores necessários para carregar o beacon no espaço de endereçamento virtual do processo.

A tabela abaixo lista os valores necessários a partir dos cabeçalhos da DLL de beacon raw, os locais onde podem ser encontrados e seus respectivos tipos:

Tabela listando valores do cabeçalho da DLL de beacon raw que são úteis para carregar a DLL de beacon.

Tabela listando valores do cabeçalho da DLL de beacon raw que são úteis para carregar a DLL de beacon.

Evasões

Nem todo o conteúdo dos cabeçalhos é necessário para carregar a DLL de beacon. Os valores obrigatórios podem ser reempacotados ou ofuscados. Os valores que não são necessários podem ser removidos ou randomizados.

Fase 3: Alocando memória para o Beacon Virtual

Assim que conhecemos o valorSizeOfImagef a partir do cabeçalho da DLL de beacon raw, precisamos alocar um bloco de memória desse tamanho. Esse espaço de memória irá conter a nossa DLL de beacon virtual.

Diferentes métodos podem ser usados para alocar memória para a DLL de beacon virtual. Cada método utiliza diferentes tipos de memória. Os diferentes métodos suportados pelo carregador refletivo padrão do Cobalt Strike são:

Tabela mostrando as opções de alocação de memória do Cobalt Strike para a DLL de beacon virtual.

Tabela mostrando as opções de alocação de memória do Cobalt Strike para a DLL de beacon virtual.

Evasões

Isso pode ser levado um passo adiante com o UDRL. A versão NTAPI dessas funções pode ser usada em vez disso. Além disso, as funções NTAPI poderiam ser chamadas por meio de chamadas de sistema diretas ou indiretas, que podem ou não ajudar a reforçar os recursos de evasão.

Quando o método do alocador é definido como VirtualAlloc  no perfil Malleable C2 do Cobalt Strike, atualmente o projeto BokuLoader usará uma chamada de sistema direta para NtAllocateVirtualMemory  para alocar memória para a DLL do beacon virtual:

Amostra de código do projeto BokuLoader mostrando uma chamada de sistema direta é usada para alocar memória para a DLL do beacon virtual.

Amostra de código do projeto BokuLoader mostrando uma chamada de sistema direta é usada para alocar memória para a DLL do beacon virtual.

  • O número de chamada do sistema é descoberto com o método HelsGate.
  • Se existir um gancho de área de usuário no stub de chamada do sistema, o método HalosGate será usado.

A imagem abaixo mostra um exemplo de código do uso dos métodos HelsGate e HalosGate para determinar os números de chamada do sistema:

Amostra de código do projeto BokuLoader mostrando como as chamadas do sistema são descobertas a partir do processo.

Amostra de código do projeto BokuLoader mostrando como as chamadas do sistema são descobertas a partir do processo.

Fase 4: Carregar seções no espaço de memória virtual

Agora que alocamos memória para nossa DLL de beacon virtual, precisamos copiar as seções do beacon de seus deslocamentos de arquivo bruto, conforme elas existem na DLL de beacon raw, para a memória alocada em seus deslocamentos virtuais relativos.

Se alocarmos nossa memória comREADWRITE precisaremos rastrear o endereço da seção .text .text e seu tamanho. Antes de chamar o ponto de entrada da DLL de beacon virtual, precisaremos alterar as proteções de memória da seção .text para executável.

Alocar nossa memória com READWRITE_EXECUTE torna o processo de carregamento refletivo mais fácil, mas aumenta as chances de detecção por soluções de segurança.

Abaixo está um exemplo de código simplificado, do projeto BokuLoader, que demonstra isso:.

Exemplo de código do projeto BokuLoader mostrando seções copiadas da DLL de beacon raw para a DLL de beacon virtual.

Exemplo de código do projeto BokuLoader mostrando seções copiadas da DLL de beacon raw para a DLL de beacon virtual.

Evasões

Algumas funcionalidades de evasão relacionadas ao carregamento de seções são:

  • Não copiar os cabeçalhos do beacon para a DLL de beacon virtual.
  • Desalocar o espaço de memória na DLL de beacon virtual onde os cabeçalhos existiriam.

No projeto público BokuLoader, os cabeçalhos da DLL de beacon não são copiados da DLL de beacon raw para a DLL de beacon virtual. Atualmente, os primeiros 0x1000  bytes da DLL de beacon virtual são nulos (0x00‘s ). Nos meus testes, o beacon não depende de seus cabeçalhos depois que ele é carregado adequadamente na memória virtual. Evitar a cópia dos cabeçalhos pode ajudar a evadir scanners em memória, mas esses bytes nulos também podem ser um possível ponto de detecção.

Outra possível oportunidade de evasão é fazer com que o script Aggressor UDRL criptografe as seções. As seções poderiam ser descriptografadas em memória pelo UDRL, usando uma chave compartilhada entre o UDRL e o script Aggressor UDRL.

Fase 5: Carregamento das dependências de DLL

O beacon HTTP/S x64 depende de quatro DLLs para funcionar corretamente. Se essas DLLs não estiverem carregadas no processo no momento, nosso carregador refletivo precisará carregá-las.

As quatro DLLs estão listadas no diretório de importação da DLLC do beacon HTTP/S:

Captura de tela do PE-Bear listando DLLs do diretório de importação da vulnerabilidade do beacon.

Captura de tela do PE-Bear listando DLLs do diretório de importação da vulnerabilidade do beacon.

O carregador refletivo Cobalt Strike integrado usa a API kernel32.LoadLibraryA para carregamento de DLL.

Evasões

O carregamento de vulnerabilidades pode ser alcançado de várias maneiras diferentes, com diferentes considerações de segurança operacional. Alguns métodos são:

Se a DLL já existir no processo, as APIs do Windows acima ainda poderão ser usadas para lidar com os endereços de base da DLL, embora isso possa desencadear alertas de detecção indesejados.

Como alternativa, o PEB contém um ponteiro para a estrutura 

<a title="https://learn.microsoft.com/br-pt/windows/win32/api/winternl/ns-winternl-peb_ldr_data" href="https://learn.microsoft.com/br-pt/windows/win32/api/winternl/ns-winternl-peb_ldr_data">_PEB_LDR_DATA</a>

. Dentro, há uma lista vinculada de todas as DLLs carregadas no processo e suas informações relativas (

InMemoryOrderModuleList

). O BokuLoader aproveita isso para descobrir as informações da DLL, evitando chamadas de API desnecessárias.

Se a DLL não existir em InMemoryOrderModuleList , atualmente o BokuLoader usa a API NTDLL.LdrLoadDll  para carregar a dependência da DLL na memória, aproveitando o carregador de biblioteca de DLL integrado do Windows.

O carregamento refletivo aninhado não pode ser usado facilmente para carregar dependências de DLL porque os carregadores refletivos geralmente não registram a DLL no processo. O código externo à DLL não pode usar corretamente uma DLL carregada de forma refletiva. O projeto DarkLoadLibrary pode ser capaz de carregar corretamente uma DLL na memória sem acionar um evento de carregamento de imagem do kernel.

Amostra de código do projeto BokuLoader mostrando como os endereços básicos carregados da DLL podem ser resolvidos percorrendo o InMemoryOrderModuleList.

Amostra de código do projeto BokuLoader mostrando como os endereços básicos carregados da DLL podem ser resolvidos percorrendo o InMemoryOrderModuleList.

Fase 6: resolvendo a tabela de endereços com importação

Com as DLLs necessárias carregadas no processo, as APIs listadas no diretório de importação devem ser resolvidas. Os endereços da API precisarão então ser escritos na Tabela de Endereços de Importação (IAT) da DLL do beacon virtual. Dessa forma, o beacon sabe para qual endereço lidar com quando precisar chamar APIs como WININET.HttpSendRequest

A entrada de importação precisará ser resolvida por meio da sequência de caracteres ordinal ou de nome.

Na imagem abaixo, vemos que a DLL do beacon do Cobalt Strike usa uma combinação de ordinais e cadeias de caracteres de nomes para entradas de importação:

Captura de tela do PE-Bear mostrando que algumas entradas de importação para a DLL de beacon devem ser resolvidas por número ordinal.

Captura de tela do PE-Bear mostrando que algumas entradas de importação para a DLL de beacon devem ser resolvidas por número ordinal.

O carregador refletivo Cobalt Strike integrado usa a API Kernel32.GetProcAddress  para lidar com endereços virtuais para entradas de importação.

Evasões

Alguns métodos de evasão para lidar com endereços de API são:

  • Implementações de código personalizado de BokuLoader GetProcAddress
  • NTDLL.LdrGetProcedureAddress

usa uma implementação de código personalizada deGetProcAddress para lidar com o endereço da entrada de importação, manipulando strings de nomes e números ordinais.

A função NTDLL.LdrGetProcedureAddress é capaz de lidar com strings de nomes e ordinais também. Se o endereço retornado para a Entrada de Importação for um encaminhador para outro DLL, o BokuLoader assume como padrão o NTDLL.LdrGetProcedureAddress para resolver o problema com o encaminhador.

Ao escrever a IAT, o hooking pode ser implementado gravando os endereços virtuais das funções de hook que implementamos, em vez dos endereços virtuais das APIs pretendidas. Desde que a saída esperada seja retornada ao beacon quando o endereço na IAT for chamado, podemos executar código adicional antes de retornar ao beacon. Publicações futuras e versões públicas do BokuLoader demonstrarão como podemos aproveitar o hooking da IAT para recursos avançados de evasão.

Com uma versão recente, o projeto público BokuLoader passou a oferecer suporte ao recurso obfuscate do Malleable PE a partir do perfil C2 do Cobalt Strike, por meio de uma implementação personalizada. Ao modificar a chave de mascaramento no script Aggressor UDRLBokuLoader.cna , a ofuscação pode ser aprimorada escolhendo a sua própria chave XOR de um único byte.

Em termos de segurança operacional, é importante saber que mecanismos de correspondência de padrões são capazes de realizar força bruta em máscaras XOR de um único byte. Publicações futuras demonstrarão como podemos criar nosso próprio mecanismo de Malleable PE usando a funcionalidade de scripts Aggressor do Cobalt Strike para ofuscar o beacon e superar mecanismos de correspondência de padrões.

Fase 7: Resolvendo realocações

A DLL do beacon possui muitas realocações que precisam ser resolvidas e gravadas na Tabela de Realocação de Base da DLL de beacon virtual antes de ela ser executada.

No PE-Bear, podemos ver que a DLL do beacon, por padrão, tem o endereço base da imagem de 0x180000000 :

Captura de tela do PE-Bear mostrando o endereço base da imagem da DLL do beacon.

Captura de tela do PE-Bear mostrando o endereço base da imagem da DLL do beacon.

Antes de começarmos a gravar as realocações, precisamos calcular o delta entre o endereço base da nossa DLL de beacon virtual e o endereço base codificado (hardcoded).

Por exemplo, vamos supor que o endereço base da nossa DLL de beacon virtual seja 0x7FFC44FE0000 . Subtraímos o endereço base codificado do endereço base da nossa DLL de beacon virtual para obter o delta do endereço base:

Captura de tela obtendo o delta de endereço base

Em seguida, para determinar o endereço virtual de cada entrada de realocação na Tabela de Realocação de Base, somamos o delta do endereço base ao endereço codificado (hardcoded) da entrada de realocação para determinar a realocação dentro da nossa DLL de beacon virtual.

Na imagem abaixo, podemos ver que as entradas de realocação de beacons são escritas ao contrário no formato pouco endian:

Captura de tela do PE-Bear mostrando que existem entradas de realocação em formato little-endian.

Captura de tela do PE-Bear mostrando que existem entradas de realocação em formato little-endian.

O endereço codificado (hardcoded) para essa entrada de realocação é 0x1800341C8 .

Somamos esse endereço ao delta do endereço base para obter o endereço virtual da realocação, conforme ela existe dentro da DLL de beacon virtual:

Captura de tela somando o endereço ao delta do endereço base para obter o endereço virtual da realocação, conforme ela existe na DLL de beacon virtual:

Para cada entrada de realocação, precisaremos verificar se o tipo é

<a title="https://learn.microsoft.com/br-pt/windows/win32/debug/pe-format" href="https://learn.microsoft.com/br-pt/windows/win32/debug/pe-format">IMAGE_REL_BASED_DIR64 (0xA)</a>

. Se isso for falso, vamos ignorar a gravação da realocação.

Assim que determinarmos o endereço virtual da realocação, conforme ela existe dentro da DLL de beacon virtual, gravamos esse valor no espaço de memória que contém o endereço codificado (hardcoded) da entrada de realocação.

Se você tiver interesse em aprender mais sobre como realizar realocações de PE, confira o código da função doRelocations no projeto público BokuLoader. Antes de publicar este post do blog, alterei o código de realocações de assembly para um código em C, espero que mais legível, para ajudar outras pessoas que queiram entender os detalhes técnicos de como isso é feito.

Fase 8: Execução do Beacon

A execução do beacon pode ser dividida em três etapas:

  • Garantir que as seções da DLL de beacon virtual tenham as permissões de memória corretas.
  • Inicializar a DLL de beacon virtual.
  • Chamar o ponto de entrada da DLL de beacon virtual.

Tornando o Beacon Virtual executável

Se a memória que alocamos para a DLL de beacon virtual for READWRITE_EXECUTE , não precisamos alterar as proteções de memória para que o beacon funcione corretamente sem falhar.

Se alocarmos a memória do beacon virtual como não executável (READWRITE ), será necessário tornar executável a seção .text  da DLL de beacon virtual. A localização e o tamanho virtual da seção .text  devem ter sido previamente salvos na função principal do UDRL como variáveis.

No projeto público BokuLoader, as alterações de proteção de memória são realizadas por meio de chamadas diretas de sistema para NTProtectVirtualMemory , conforme mostrado no exemplo de código abaixo:

Captura de tela feita para a postagem do blogExemplo de código do projeto BokuLoader demonstrando a alteração da seção .text da DLL de beacon virtual para executável.

Captura de tela feita para a postagem do blogExemplo de código do projeto BokuLoader demonstrando a alteração da seção .text da DLL de beacon virtual para executável.

O .data  na seção da DLL de beacon virtual deve ter permissõesREADWRITE . Caso essa seção não seja gravável, a DLL de beacon pode falhar durante a execução.

Inicializando a DLL de Beacon Virtual

Para que a DLL de beacon virtual seja executada corretamente, ela deve primeiro ser inicializada chamando o ponto de entrada da DLL de beacon virtual. O primeiro argumento é o endereço base da DLL de beacon virtual. O segundo argumento é o fwdReason  , que deve ser definido como DLL_PROCESS_ATTACH (1) .

Amostra de código do projeto BokuLoader inicializando a DLL do beacon virtual.

Amostra de código do projeto BokuLoader inicializando a DLL do beacon virtual.

Executando nossa DLLC do Virtual Beacon

Após inicializar a DLL de beacon virtual, podemos optar por retornar o ponto de entrada do beacon virtual ao stub do Carregador Refletivo de Chamada, ou chamar o ponto de entrada da DLL de beacon virtual diretamente no nosso UDRL, com ofwdReason  definido como 0x4 .

Diferentemente de uma DLL típica, em que o primeiro argumento hinstDLL  para 

<a href="https://learn.microsoft.com/br-pt/windows/win32/dlls/dllmain">DLLMAIN</a>

seria o endereço base da DLL virtual, o beacon espera receber o endereço base da DLL de beacon raw. Caso isso não seja fornecido, alguns recursos de evasão do Malleable PE podem falhar.

Exemplo de código do projeto BokuLoader mostrando duas formas diferentes de executar a DLL de beacon virtual.

Exemplo de código do projeto BokuLoader mostrando duas formas diferentes de executar a DLL de beacon virtual.

Considerações finais

Espera-se que este post de blog ajude tanto equipes red team quanto blue team a compreender melhor o Cobalt Strike e o processo de carregamento refletivo. Ainda existem inúmeras oportunidades de evasão que podem ser implementadas por meio do carregamento refletivo. Com uma compreensão mais profunda desses conceitos, as organizações podem se preparar melhor para uma defesa eficaz contra ameaças cibernéticas.

Os próximos posts desta série terão foco na integração do UDRL com os recursos atuais de evasão do Cobalt Strike, na exploração de recursos de evasão não documentados já presentes no BokuLoader público, bem como em funcionalidades avançadas que ainda não foram disponibilizadas publicamente. Fique atento para mais informações detalhadas e técnicas avançadas sobre como elevar o uso do Cobalt Strike a um novo patamar por meio do desenvolvimento de UDRL.

Mixture of Experts | 12 de dezembro, episódio 85

Decodificando a IA: resumo semanal das notícias

Participe do nosso renomado painel de engenheiros, pesquisadores, líderes de produtos e outros enquanto filtram as informações sobre IA para trazerem a você as mais recentes notícias e insights sobre IA.