Ataques Direct Kernel Object Manipulation (DKOM) a provedores de ETW.

Retrato de vista lateral de um homem olhando para a tela do computador enquanto trabalha até tarde da noite

Autor

Ruben Boonen

CNE Capability Lead, Adversary Services

IBM X-Force

Neste artigo, os hackers ofensivos do IBM Security X-Force Red analisam como os invasores, com privilégios elevados, podem usar o acesso para preparar recursos do kernel do Windows após uma invasão. Nos últimos anos, as contas públicas têm mostrado cada vez mais que invasores menos sofisticados estão usando essa técnica para atingir seus objetivos. Portanto, é importante destacarmos esse recurso e sabermos mais sobre seu possível impacto. Especificamente, neste post, avaliaremos como a invasão do Kernel pode ser usada para ocultar sensores ETW e vincular isso às amostras de malware identificadas no ano passado.

Introdução

Com o tempo, as mitigações de segurança e a telemetria de detecção no Windows melhoraram bastante. Quando esses recursos são combinados com soluções de Endpoint Detection & Response (EDR) bem configuradas, podem representar uma barreira considerável à pós-invasão. Os invasores têm um custo constante para desenvolver e aprimorar táticas, técnicas e procedimentos (TTPs) para evitar heurísticas de detecção. Na equipe de simulação de adversários do IBM Security X-Force, enfrentamos o mesmo problema. Nossa equipe tem a tarefa de simular recursos avançados de ameaças em alguns dos maiores e mais protegidos ambientes. Soluções de segurança sofisticadas e bem calibradas, aliadas a equipes de Security Operations Center (SOC) bem treinadas podem elevar o custo e a complexidade do tradecraft. Em alguns casos, o uso de uma TTP específica torna-se completamente obsoleta em um período de três a quatro meses (geralmente associada a um stack de tecnologias específico).

Os invasores podem optar por aproveitar a execução de código no kernel do Windows para adulterar algumas dessas proteções ou para evitar totalmente uma série de sensores do ambiente do usuário. A primeira demonstração publicada de tais recursos foi em 1999 na Phrack Magazine. Nos anos seguintes, houve vários relatos de casos em que os agentes da ameaça (TAs) usaram rootkits de Kernel para o pós-invasão. Alguns exemplos mais antigos incluem o Derusbi Family e o Lamberts toolkit.

Tradicionalmente, esses tipos de recursos têm sido limitados a TAs avançados. Nos últimos anos, porém, vimos mais invasores comuns usarem primitivas de invasão Bring Your Own Vulnerable Driver (BYOVD) para facilitar ações no endpoint. Em alguns casos, essas técnicas foram bastante primitivas, limitadas a tarefas simples, mas também houve demonstrações mais sofisticadas.

No final de setembro de 2022, uma pesquisa da ESET publicou um relatório técnico sobre recursos do kernel utilizados pelo agente de ameaça Lazarus em diversos ataques contra entidades na Bélgica e na Holanda com o objetivo de exfiltrar dados. Este artigo apresenta uma série de primitivas de Direct Kernel Object Manipulation (DKOM) que a carga útil usa para ocultar a telemetria de OS/AV/EDR. A pesquisa pública disponível sobre essas técnicas é escassa. Adquirir uma compreensão mais aprofundada das técnicas de pós-invasão no kernel é fundamental para a defesa. Um argumento clássico e ingênuo que se ouve frequentemente é que um invasor com privilégios elevados pode fazer qualquer coisa, então por que deveríamos desenvolver recursos nesse cenário? Essa é uma postura fraca. Os defensores precisam entender quais recursos um invasor possui quando seus privilégios são elevados, quais fontes de dados permanecem confiáveis (e quais não), quais opções de contenção existem e como técnicas avançadas podem ser detectadas (mesmo que os recursos para realizar essa detecção não existam). Neste artigo, focarei especificamente na aplicação de patches nas estruturas do Kernel Event Tracing for Windows (ETW) para tornar os provedores ineficazes ou inoperáveis. Vou apresentar algumas informações básicas sobre essa técnica, analisar como um invasor pode manipular estruturas de ETW do Kernel e abordar alguns mecanismos para encontrar essas estruturas. Por fim, analisarei como essa técnica foi implementada pelo Lazarus na carga útil.

As mais recentes notícias de tecnologia, corroboradas por insights de especialistas.

Mantenha-se atualizado sobre as tendências mais importantes (e intrigantes) do setor em IA, automação, dados e muito mais com o boletim informativo Think. Consulte a Declaração de privacidade da IBM.

Agradecemos sua inscrição!

Sua assinatura será entregue em inglês. Você pode encontrar um link para cancelar a assinatura em todos os boletins informativos. Você pode gerenciar suas inscrições ou cancelar a inscrição aqui. Consulte nossa Declaração de privacidade da IBM para obter mais informações.

ETW DKOM

O ETW é uma ferramenta de rastreamento de alta velocidade integrada ao sistema operacional Windows. Ele permite o registro de eventos e atividades do sistema por aplicação, drivers e sistema operacional, fornecendo visibilidade detalhada do comportamento do sistema para depuração, análise de desempenho e diagnóstico de segurança.

Nesta seção, apresentarei uma visão geral de alto nível do Kernel ETW e sua superfície de ataque associada. Isso será útil para ter uma melhor compreensão dos mecanismos envolvidos na manipulação de provedores de ETW e os efeitos associados a essas manipulações.

Superfície de ataque do ETW no kernel

Pesquisadores da Binarly deram uma palestra no BHEU 2021, na qual discutiram a superfície de ataque geral do ETW no Windows. Veja abaixo a visão geral do modelo de ameaça.

fluxograma mostrando a modelagem de ameaças de ETW
Figura 1 – Veni, No Vidi, No Vici: ataques ao ETW para cegar sensores de EDR (Binarly)

Nesta postagem, vamos nos concentrar na superfície de ataque do espaço do Kernel.

um gráfico que mostra e descreve ataques a provedores de ETW em modo kernel
Figura 2 – Veni, No Vidi, No Vici: ataques ao ETW para cegar sensores de EDR (Binarly)

Esta publicação considera apenas os ataques dentro da primeira categoria de ataques mostrada na "Figura 2", na qual o rastreamento é desabilitado ou alterado de alguma forma.

Como observação, ao considerar estruturas opacas no Windows, é sempre importante lembrar que elas estão sujeitas a alterações e, de fato, frequentemente mudam entre as versões do Windows. Isso é especialmente importante ao sobrescrever dados do Kernel, pois erros provavelmente resultarão em uma Blue Screen of Death (BSoD), então proceda com segurança!

Inicialização

Os provedores de kernel são registrados usando nt!EtwRegister, uma função exportada pelo ntoskrnl. Uma versão descompilada da função pode ser vista abaixo.

captura de tela do código para descompilação do nt!EtwRegister
Figura 3 – descompilação do nt!EtwRegister

A inicialização completa ocorre na função interna EtwpRegisterKMProvider, mas há dois pontos principais aqui:

  • O ProviderId é um ponteiro para um GUID de 16 bytes. Esse GUID é estático em todos os sistemas operacionais e, portanto, pode ser usado para identificar o provedor que está sendo inicializado.
  • O RegHandle é um endereço de memória que recebe um ponteiro para uma estrutura _ETW_REG_ENTRY em uma chamada bem-sucedida. Essa estrutura de dados e algumas de suas propriedades aninhadas oferecem meios para manipular o provedor ETW, conforme pesquisa da Binarly.

Vamos listar brevemente as estruturas que a Binarly destacou no slide da Figura 2.

ETW_REG_ENTRY

A listagem completa de 64 bits da estrutura _ETW_REG_ENTRY é mostrada abaixo. Mais detalhes estão disponíveis no blog do Geoff Chappell aqui. Essa estrutura também pode ser explorada mais a fundo no Vergilius Project.

// 0x70 bytes (sizeof)
// Win11 22H2 10.0.22621.382
struct _ETW_REG_ENTRY
{
    struct _LIST_ENTRY RegList;                           //0x0
    struct _LIST_ENTRY GroupRegList;                      //0x10
    struct _ETW_GUID_ENTRY* GuidEntry;                    //0x20
    struct _ETW_GUID_ENTRY* GroupEntry;                   //0x28
    union
    {
        struct _ETW_REPLY_QUEUE* ReplyQueue;              //0x30
        struct _ETW_QUEUE_ENTRY* ReplySlot[4];            //0x30
        struct
        {
            VOID* Caller;                                 //0x30
            ULONG SessionId;                              //0x38
        };
    };
    union
    {
        struct _EPROCESS* Process;                        //0x50
        VOID* CallbackContext;                            //0x50
    };
    VOID* Callback;                                       //0x58
    USHORT Index;                                         //0x60
    union
    {
        USHORT Flags;                                     //0x62
        struct
        {
            USHORT DbgKernelRegistration:1;               //0x62
            USHORT DbgUserRegistration:1;                 //0x62
            USHORT DbgReplyRegistration:1;                //0x62
            USHORT DbgClassicRegistration:1;              //0x62
            USHORT DbgSessionSpaceRegistration:1;         //0x62
            USHORT DbgModernRegistration:1;               //0x62
            USHORT DbgClosed:1;                           //0x62
            USHORT DbgInserted:1;                         //0x62
            USHORT DbgWow64:1;                            //0x62
            USHORT DbgUseDescriptorType:1;                //0x62
            USHORT DbgDropProviderTraits:1;               //0x62
        };
    };
    UCHAR EnableMask;                                     //0x64
    UCHAR GroupEnableMask;                                //0x65
    UCHAR HostEnableMask;                                 //0x66
    UCHAR HostGroupEnableMask;                            //0x67
    struct _ETW_PROVIDER_TRAITS* Traits;                  //0x68
};

ETW_GUID_ENTRY

Uma das entradas aninhadas dentro do _ETW_REG_ENTRY é GuidEntry, que é uma estrutura _ETW_GUID_ENTRY. Mais informações sobre essa estrutura não documentada podem ser encontradas no blog do Geoff Chappell aqui e no Vergilius Project.

// 0x1a8 bytes (sizeof)
// Win11 22H2 10.0.22621.382
struct _ETW_GUID_ENTRY
{
    struct _LIST_ENTRY GuidList;                          //0x0
    struct _LIST_ENTRY SiloGuidList;                      //0x10
    volatile LONGLONG RefCount;                           //0x20
    struct _GUID Guid;                                    //0x28
    struct _LIST_ENTRY RegListHead;                       //0x38
    VOID* SecurityDescriptor;                             //0x48
    union
    {
        struct _ETW_LAST_ENABLE_INFO LastEnable;          //0x50
        ULONGLONG MatchId;                                //0x50
    };
    struct _TRACE_ENABLE_INFO ProviderEnableInfo;         //0x60
    struct _TRACE_ENABLE_INFO EnableInfo[8];              //0x80
    struct _ETW_FILTER_HEADER* FilterData;                //0x180
    struct _ETW_SILODRIVERSTATE* SiloState;               //0x188
    struct _ETW_GUID_ENTRY* HostEntry;                    //0x190
    struct _EX_PUSH_LOCK Lock;                            //0x198
    struct _ETHREAD* LockOwner;                           //0x1a0
};

TRACE_ENABLE_INFO

Por fim, uma das entradas aninhadas em _ETW_GUID_ENTRY é ProviderEnableInfo, que é uma estrutura _TRACE_ENABLE_INFO. Para mais informações sobre os elementos dessa estrutura de dados, você pode consultar a documentação oficial da Microsoft e o Vergilius Project. As configurações dessa estrutura afetam diretamente as operações e os recursos do provedor.

// 0x20 bytes (sizeof)
// Win11 22H2 10.0.22621.382
struct _TRACE_ENABLE_INFO
{
    ULONG IsEnabled;                                       //0x0
    UCHAR Level;                                           //0x4
    UCHAR Reserved1;                                       //0x5
    USHORT LoggerId;                                       //0x6
    ULONG EnableProperty;                                  //0x8
    ULONG Reserved2;                                       //0xc
    ULONGLONG MatchAnyKeyword;                             //0x10
    ULONGLONG MatchAllKeyword;                             //0x18
};

Entendendo o uso do identificador de registro

Embora algum conhecimento teórico seja útil, é sempre melhor analisar exemplos práticos para obter uma compreensão mais profunda do assunto. Vejamos brevemente um exemplo. A maioria dos provedores críticos de ETW do Kernel é inicializada dentro de, nt!EtwpInitialize, que não é exportado. A análise dessa função revela cerca de quinze provedores.

Captura de tela do código para descompilação parcial nt!EtwpInitialize
Figura 4 – descompilação parcial nt!EtwpInitialize

Tomando como exemplo a entrada Microsoft-Windows-Threat-Intelligence (EtwTi), podemos verificar o parâmetro global ThreatIntProviderGuid para recuperar o GUID desse provedor.

Captura de tela do código para o GUID do provedor EtwTi
Figura 5 – GUID do provedor EtwTi

A busca deste GUID on-line revelará imediatamente que conseguimos recuperar o valor correto (f4e1897c-bb5d-5668-f1d8-040f4d8dd344).

Vamos examinar uma instância em que o parâmetro do identificador de registro, EtwThreatIntProvRegHandle, é usado e analisar como ele é usado. Um local em que o identificador é referenciado é nt!EtwTiLogDriverObjectUnLoad. Pelo nome desta função, podemos intuir que ela se destina a gerar eventos quando um objeto de driver é descarregado pelo Kernel.

Captura de tela do código para descompilação do nt!EtwTiLogDriverUnload
Figura 6 – descompilação nt!EtwTiLogDriverUnload

As funções nt!EtwEventEnabled e nt!EtwProviderEnabled são chamadas aqui, passando o identificador de registro como um dos argumentos. Vamos dar uma olhada em uma dessas subfunções para entender melhor o que está acontecendo.

Captura de tela do código para descompilação do nt!EtwProviderEnable
Figura 7 - descompilação do nt!EtwProviderEnable

Admito que isso é um pouco difícil de acompanhar. No entanto, a aritmética do ponteiro não é especialmente importante. Em vez disso, vamos nos concentrar em como essa função processa o identificador do registro. Aparentemente, a função valida diversas propriedades da estrutura _ETW_REG_ENTRY e suas subestruturas, como a propriedade GuidEntry .

struct _ETW_REG_ENTRY
{
    …
    struct _ETW_GUID_ENTRY* GuidEntry;                    //0x20
    …
}

E a propriedade GuidEntry->ProviderEnableInfo.

struct _ETW_GUID_ENTRY
{
    …
    struct _TRACE_ENABLE_INFO ProviderEnableInfo;         //0x60
    …
}

A função então passa a realizar verificações semelhantes baseadas em níveis. Por fim, a função retorna verdadeiro ou falso para indicar se um provedor está habilitado para registro de eventos em um nível e palavra-chave especificados. Mais detalhes estão disponíveis na documentação oficial da Microsoft.

Podemos ver que, quando um provedor é acessado por meio de seu identificador de registro, a integridade dessas estruturas se torna muito importante para as operações do provedor. Por outro lado, se um invasor fosse capaz de manipular essas estruturas, ele poderia influenciar o fluxo de controle do chamador para descartar ou impedir que eventos fossem registrados.

Atacando identificadores de registro

Analisando a superfície de ataque declarada pela Binarly e com base em nossa análise superficial, podemos propor algumas estratégias para interromper a coleta de eventos.

  • Um invasor pode atribuir um valor NULL ao ponteiro _ETW_REG_ENTRY. Qualquer função que faça referência ao identificador de registro assumirá, então, que o provedor não foi inicializado.
  • Um invasor pode atribuir um valor NULL ao ponteiro _ETW_REG_ENTRY->GuidEntry->ProviderEnableInfo. Isso deve desativar efetivamente os recursos de coleta do provedor, pois ProviderEnableInfo é um ponteiro para uma estrutura _TRACE_ENABLE_INFO que descreve como o provedor deve operar.
  • Um invasor pode sobrescrever propriedades da estrutura de dados _ETW_REG_ENTRY->GuidEntry->ProviderEnableInfo para adulterar a configuração do provedor.
    • IsEnabled: defina como 1 para ativar o recebimento de eventos do provedor ou para ajustar as configurações usadas ao receber eventos do provedor. Defina como 0 para desativar o recebimento de eventos do provedor.
    • Level: um valor que indica o nível máximo de eventos que você deseja que o provedor grave. Normalmente, o provedor grava um evento se o nível do evento for menor ou igual a esse valor, além de atender aos critérios MatchAnyKeyword e MatchAllKeyword.
    • MatchAnyKeyword: máscara de bits de 64 bits de palavras-chave que determinam as categories de eventos que você deseja que o provedor registre. Normalmente, o provedor grava um evento se os bits de palavra-chave do evento corresponderem a qualquer um dos bits definidos nesse valor ou se o evento não tiver bits de palavra-chave definidos, além de atender aos critérios Level e MatchAllKeyword.
    • MatchAllKeyword: máscara de bits de 64 bits de palavras-chave que restringe os eventos que você deseja que o provedor escreva. Normalmente, o provedor grava um evento se os bits de palavra-chave do evento corresponderem a todos os bits definidos nesse valor ou se o evento não tiver bits de palavra-chave definidos, além de atender aos critérios Level e MatchAnyKeyword.

Técnicas de busca no kernel

Agora temos uma boa ideia de como é um ataque DKOM ao ETW. Vamos supor que o invasor tenha uma vulnerabilidade que conceda uma primitiva de leitura/gravação do kernel, como o malware Lazarus faz nesse caso ao carregar um driver vulnerável. O que falta é uma maneira de encontrar esses identificadores de registro.

Vou descrever duas técnicas principais para encontrar esses identificadores e mostrar a variante de uma delas que é usada pelo Lazarus em sua carga útil do kernel.

Bypass de KASLR em nível de integridade médio (MedIL)

Em primeiro lugar, pode ser prudente explicar que, embora exista o ASLR do Kernel, isso não representa uma barreira de segurança para invasores locais, caso eles consigam executar código no MedIL ou em níveis superiores. Há muitas maneiras de vazar ponteiros do Kernel que são restritas apenas em área de testes ou LowIL. Para obter informações básicas, você pode consultar o livro "I Got 99 Problems But a Kernel Pointer Ain't One" do Alex Lonescu; muitas dessas técnicas ainda são aplicáveis hoje em dia.

A ferramenta mais adequada aqui é ntdll!NtQuerySystemInformation com a classe SystemModuleInformation:

internal static UInt32 SystemModuleInformation = 0xB;

[DllImport(“ntdll.dll”)]
internal static extern UInt32 NtQuerySystemInformation(
    UInt32 SystemInformationClass,
    IntPtr SystemInformation,
    UInt32 SystemInformationLength,
    ref UInt32 ReturnLength);

Essa função retorna o endereço base ativo de todos os módulos carregados no espaço de kernel. Nesse ponto, é possível analisar esses módulos em disco e converter deslocamentos brutos do arquivo em endereços virtuais relativos, e vice-versa.

public static UInt64 RvaToFileOffset(UInt64 rva, List<SearchTypeData.IMAGE_SECTION_HEADER> sections)
{
    foreach (SearchTypeData.IMAGE_SECTION_HEADER section in sections)
    {
        if (rva >= section.VirtualAddress && rva < section.VirtualAddress + section.VirtualSize)
        {
            return (rva – section.VirtualAddress + section.PtrToRawData);
        }
    }
    return 0;
}

public static UInt64 FileOffsetToRVA(UInt64 fileOffset, List<SearchTypeData.IMAGE_SECTION_HEADER> sections)
{
    foreach (SearchTypeData.IMAGE_SECTION_HEADER section in sections)
    {
        if (fileOffset >= section.PtrToRawData && fileOffset < (section.PtrToRawData + section.SizeOfRawData))
        {
            return (fileOffset – section.PtrToRawData) + section.VirtualAddress;
        }
    }
    return 0;
}

Um invasor também pode carregar esses módulos em seu processo em modo usuário utilizando chamadas padrão da API de carregamento de bibliotecas (por exemplo, ntdll!LdrLoadDll). Isso evitaria complicações na conversão de deslocamentos de arquivos em RVAs e vice-versa. No entanto, do ponto de vista da segurança operacional (OpSec), isso não é o ideal, pois pode gerar mais telemetria de detecção.

Método 1: cadeias de gadgets

Sempre que possível, essa é a técnica que prefiro, pois torna os vazamentos mais portáteis entre as versões do módulo, já que são menos afetados pelas alterações de patches. A desvantagem é que você depende de uma cadeia de gadgets existente para o objeto que deseja vazar.

Considerando os identificadores de registro ETW, vamos pegar como exemplo o Microsoft-Windows-Threat-Intelligence. Abaixo, você pode ver a chamada completa para o nt!EtwRegister.

Captura de tela do código para desmontagem completa da CALL do nt!EtwRegister
Figura 8 – desmontagem completa da CALL nt!EtwRegister

Aqui, queremos vazar o ponteiro para o identificador de registro, EtwThreatIntProvRegHandle. Conforme carregado no parâmetro param_4, na primeira linha da figura 8.Este ponteiro resolve para uma variável global dentro da seção .data do módulo Kernel. Como essa chamada ocorre em uma função não exportada, não podemos vazar seu endereço diretamente. Em vez disso, precisamos verificar onde essa variável global é referenciada e ver se ela é usada em uma função cujo endereço pode vazar.

captura de tela do código para referências de nt!EtwThreatIntProvRegHandle
Figura 9 – referências de nt!EtwThreatIntProvRegHandle

Explorar algumas dessas entradas revela rapidamente um candidato em nt!KeInsertQueueApc.

captura de tela do código para descompilação parcial do nt!KeInsertQueueApc
Figura 10 – descompilação parcial de nt!KeInsertQueueApc

Este é um ótimo candidato por alguns motivos:

  • nt!KeInsertQueueApc é uma função exportada. Isso significa que podemos vazar seu endereço em tempo de execução utilizando um bypass de KASLR. Depois, podemos usar nossa vulnerabilidade do Kernel para ler os dados nesse endereço.
  • A variável global é usada no início da função. Isso é muito útil porque significa que provavelmente não precisaremos construir uma lógica complexa de análise de instruções para encontrá-la.

A montagem mostra o seguinte layout.

captura de tela do código para desmontagem parcial do nt!KeInsertQueueApc
Figura 11 – desmontagem parcial do nt!KeInsertQueueApc

Vazar esse identificador de registro torna-se, então, simples. Utilizando nossa vulnerabilidade, lemos um array de bytes e procuramos a primeira instrução mov R10 para calcular o deslocamento virtual relativo da variável global. O cálculo seria mais ou menos assim:

Int32 pOffset = Marshal.ReadInt32((IntPtr)(pBuff.ToInt64() + i + 3));
hEtwTi = (IntPtr)(pOffset + i + 7 + oKeInsertQueueApc.pAddress.ToInt64());

Com o identificador de registro, é possível acessar a estrutura de dados _ETW_REG_ENTRY.

Em geral, essas cadeias de gadgets podem ser usadas para vazar uma variedade de estruturas de dados do Kernel. No entanto, vale ressaltar que nem sempre é possível encontrar essas cadeias de gadgets e, às vezes, elas podem ter vários estágios complexos. Por exemplo, uma possível cadeia de gadgets para vazar constantes de page directory entry (PDE) poderia ficar assim.

MmUnloadSystemImage -> MiUnloadSystemImage -> MiGetPdeAddress

Na verdade, uma análise superficial dos identificadores de registro ETW revelou que a maioria não tem cadeias de gadgets adequadas que possam ser usadas conforme descrito acima.

Método 2: varredura da memória

A outra opção principal para vazar esses identificadores de registro ETW é usar a varredura de memória, seja da memória do Kernel ativo ou de um módulo no disco. Lembre-se de que ao escanear módulos em disco, é possível converter deslocamentos de arquivos em RVAs.

Essa abordagem consiste em identificar padrões de bytes exclusivos, escanear esses padrões e, finalmente, realizar algumas operações em deslocamentos da correspondência de padrões. Vamos analisar novamente o nt!EtwpInitialize para entender melhor:

Captura de tela do código para descompilação parcial nt!EtwpInitialize
Figura 12 – descompilação parcial nt!EtwpInitialize

Todas as quinze chamadas para nt!EtwRegister estão, em sua maioria, agrupadas nesta função. A estratégia principal aqui é encontrar um padrão único que apareça antes da primeira chamada para nt!EtwRegister e um segundo padrão que apareça após a última chamada para nt!EtwRegister. Isso não é muito complexo. Um truque que pode ser usado para melhorar a portabilidade é criar um scanner de padrões que seja capaz de lidar com strings de bytes curingas. Essa é uma tarefa que fica a cargo do leitor.

Uma vez identificados os índices de início e fim, é possível examinar todas as instruções intermediárias.

  • As instruções CALL em potencial podem ser identificadas com base no código de operação para CALL, que é 0xe8.
  • Em seguida, uma leitura de tamanho DWORD é usada para calcular o deslocamento relativo da possível instrução CALL.
  • Esse deslocamento é então adicionado ao endereço relativo da CALL e incrementado em cinco (o tamanho da instrução de montagem).
  • Por fim, esse novo valor pode ser comparado a nt!EtwRegister para encontrar todos os locais de CALL válidos.

Após encontrar todas as instruções de CALL, é possível pesquisar retroativamente e extrair os argumentos da função: primeiro, o GUID que identifica o provedor ETW e, segundo, o endereço do identificador de registro. Com essas informações em mãos, podemos realizar ataques DKOM informados nos identificadores de registro para afetar a operação dos provedores identificados.

Aplicação de patches em ETW pelo Lazarus

Peguei uma amostra da DLL FudModle mencionada no whitepaper da ESET e a analisei. Essa DLL carrega um driver Dell vulnerável assinado (de um recurso codificado em XOR inline) e, em seguida, conduz o driver para corrigir várias estruturas do Kernel a fim de limitar a telemetria no host.

Captura de tela do código para o hash FudModule do Lazarus
Figura 13 – hash do Lazarus FudModule

Como parte final deste post, quero avaliar a estratégia que o Lazarus utiliza para encontrar os identificadores de registro ETW do kernel. É uma variação do método de varredura que discutimos acima.

No início da função de busca, o Lazarus resolve o nt!EtwRegister e usa esse endereço para iniciar a varredura

Captura de tela do código com a descompilação parcial da busca de ETW no FudModule do Lazarus
Figura 14 – descompilação parcial da busca por ETW no FudModule do Lazarus

Essa decisão é um tanto estranha, pois depende de onde a função existe em relação a onde ela é chamada. A posição relativa de uma função em um módulo pode variar de versão para versão, já que um novo código pode ser introduzido, removido ou alterado. No entanto, devido à maneira como os módulos são compilados, espera-se que as funções mantenham uma ordem relativamente estável. Presume-se que isso seja uma otimização da velocidade de busca.

Ao procurar referências nt!EtwRegister no ntoskrnl, parece que não se perdem muitas entradas usando essa técnica. O Lazarus também pode ter realizado análises adicionais para determinar que as entradas perdidas não são importantes ou não precisam ser corrigidas. As entradas perdidas estão destacadas abaixo. Ao empregar essa estratégia, o Lazarus consegue ignorar 0x7b1de0 bytes durante a varredura, o que pode representar uma quantidade significativa se a varredura for lenta.

Captura de tela do código para instâncias de chamadas para nt!EtwRegister
Figura 15 – instâncias de chamadas para nt!EtwRegister

Além disso, ao iniciar a varredura, as cinco primeiras correspondências são ignoradas antes de começar a registrar os identificadores de registro. Parte da função de busca é mostrada abaixo.

Captura de tela do código com a descompilação parcial da busca de ETW no FudModule do Lazarus
Figura 16 – descompilação parcial da busca por ETW no FudModule do Lazarus

O código é um pouco obscuro, mas conseguimos obter os pontos principais. O código procura por chamadas para nt!EtwRegister, extrai o identificador de registro, converte esse identificador para o endereço ativo usando um bypass do KASLR e armazena o ponteiro em um array reservado para esse propósito dentro de uma estrutura de configuração de malware (alocada na inicialização).

Por fim, vamos dar uma olhada no que o Lazarus faz para desabilitar esses provedores.

Captura de tela do código para os identificadores de registro NULL ETW no FudModule do Lazarus
Figura 17 – identificadores de registro ETW NULL no FudModule do Lazarus

Isso faz bastante sentido; o que o Lazarus faz aqui é vazar a variável global que vimos anteriormente e, em seguida, sobrescrever o ponteiro nesse endereço com NULL. Isso apaga efetivamente a referência à estrutura de dados _ETW_REG_ENTRY, se ela existir.

Não estou completamente satisfeito com as técnicas demonstradas por alguns motivos:

  • A carga útil não captura GUIDs do provedor, portanto, não pode tomar nenhuma decisão inteligente sobre sobrescrever ou não o identificador de registro do provedor.
  • A decisão de iniciar a varredura em um deslocamento dentro do ntoskrnl parece questionável porque o deslocamento da varredura pode variar dependendo da versão do ntoskrnl.
  • Ignorar arbitrariamente as primeiras 5 correspondências parece igualmente questionável. Pode haver razões estratégicas para essa decisão, mas uma abordagem melhor seria primeiro coletar todos os provedores e depois usar alguma lógica programática para filtrar os resultados.
  • Sobrescrever o ponteiro para _ETW_REG_ENTRY deve funcionar, mas essa técnica é um pouco óbvia. Seria melhor sobrescrever as propriedades de _ETW_REG_ENTRY ou _ETW_GUID_ENTRY ou _TRACE_ENABLE_INFO.

Reimplementei essa técnica para fins de pesquisa; porém, fiz alguns ajustes nas técnicas empregadas.

  • Um algoritmo de busca com velocidade otimizada é usado para localizar todos os bytes 0xe8 no ntoskrnl.
  • Em seguida, é realizado um pós-processamento para determinar quais dessas instruções de CALL são válidas e seus respectivos destinos.
  • Nem todas as chamadas para nt!EtwRegister são úteis porque, às vezes, a função é chamada com um argumento dinâmico para o identificador de registro. Por isso, é necessária alguma lógica extra para filtrar as chamadas restantes.
  • Por fim, todos os GUIDs são resolvidos para sua forma legível por humanos e os identificadores de registro são enumerados.

No geral, após ajustes, a técnica acima é claramente a melhor maneira de realizar esse tipo de enumeração. Como o tempo de busca é insignificante com algoritmos otimizados, faz sentido verificar todo o módulo no disco e, em seguida, usar alguma lógica adicional pós-verificação para filtrar os resultados.

Impacto ETW DKOM

É prudente avaliar brevemente o impacto que esse ataque pode ter. Quando os dados do provedor são reduzidos ou totalmente eliminados, há uma perda de informações, mas ao mesmo tempo nem todos os provedores sinalizam eventos sensíveis à segurança.

Alguns subconjuntos desses provedores, no entanto, são sensíveis à segurança. O exemplo mais óbvio disso é o Microsoft-Windows-Threat-Intelligence (EtwTi), que é uma fonte central de dados para o Microsoft Defender Advanced Threat Protection (MDATP), que agora é chamado de Defender for Endpoint (tudo isso é muito confuso). É importante ressaltar que o acesso a esse provedor é altamente restrito, apenas os drivers do Early Launch Anti Malware (ELAM) são capazes de se registrar nesse provedor. Da mesma forma, os processos do ambiente do usuário que recebem esses eventos devem ter um status protegido (ProtectedLight / Antimalware) e devem ser assinados com o mesmo certificado do driver ELAM.

Usando o EtwExplorer, é possível ter uma ideia melhor dos tipos de informações que esse provedor pode sinalizar.

Captura de tela da página de arquivo ETW Explorer
Figura 18 – ETW Explorer

O manifesto XML é muito grande para ser incluído aqui em sua integridade, mas um evento é mostrado abaixo para dar uma ideia dos tipos de dados que podem ser suprimidos usando o DKOM.

Captura de tela do código para o manifesto XML parcial do EtwTi
Figura 19 – manifesto XML parcial do EtwTi

Conclusão

O Kernel foi e continua sendo uma área importante e controversa, em que a Microsoft e provedores terceirizados precisam se esforçar para proteger a integridade do sistema operacional. A corrupção de dados no Kernel não é apenas uma funcionalidade da pós-invasão, mas também um componente central no desenvolvimento da exploração do Kernel. A Microsoft já fez muitos progressos nessa área com a introdução do Virtualization Based Security (VBS) e de um de seus componentes como o Kernel Data Protection (KDP).

Os consumidores do sistema operacional Windows, por sua vez, precisam aproveitar esses avanços para impor o máximo de custo possível aos potenciais invasores. O Windows Defender Application Control (WDAC) pode ser usado para garantir que as proteções VBS estejam funcionando e que existam políticas que proíbam o carregamento de drivers potencialmente perigosos.

Esses esforços são ainda mais importantes à medida que vemos cada vez mais os TAs aproveitarem os ataques BYOVD para realizar DKOM no espaço do Kernel.

 

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.

Referências adicionais

  • Veni, No Vidi, No Vici: Attacks on ETW Blind EDR Sensors (BHEU 2021 Slides) – aqui
  • Veni, No Vidi, No Vici: Attacks on ETW Blind EDR Sensors (BHEU 2021 Video) – aqui
  • Advancing Windows Security (BlueHat Shanghai 2019) – aqui
  • Exploiting a “Simple” Vulnerability – In 35 Easy Steps or Less! – aqui
  • Exploiting a “Simple” Vulnerability – Part 1.5 – The Info Leak – aqui
  • Introduction to Threat Intelligence ETW – aqui
  • TelemetrySourcerer – aqui
  • WDAC Policy Wizard – aqui

Saiba mais sobre o X-Force Red aqui. Agende uma consulta sem custo com o X-Force aqui.

Soluções relacionadas
Soluções de segurança corporativa

Transforme seu programa de segurança com soluções do maior provedor de segurança corporativa.

Explore as soluções de cibersegurança
Serviços de cibersegurança

Transforme sua empresa e gerencie riscos com consultoria em cibersegurança, nuvem e serviços de segurança gerenciados.

    Conheça os serviços de segurança cibernética
    Cibersegurança de inteligência artificial (IA)

    Melhore a velocidade, a precisão e a produtividade das equipes de segurança com soluções de cibersegurança impulsionadas por IA.

    Explore a cibersegurança da IA
    Dê o próximo passo

    Quer você necessite de soluções de segurança de dados, gerenciamento de endpoints ou gerenciamento de acesso e identidade (IAM), nossos especialistas estão prontos para trabalhar com você para alcançar uma postura de segurança forte. Transforme sua empresa e gerencie os riscos com um líder mundial em consultoria de cibersegurança, nuvem e serviços de segurança gerenciados.

    Explore as soluções de cibersegurança Descubra os serviços de cibersegurança