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.
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.
Boletim informativo do setor
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.
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.
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.
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.
Nesta postagem, vamos nos concentrar na superfície de ataque do espaço do Kernel.
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!
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.
A inicialização completa ocorre na função interna EtwpRegisterKMProvider, mas há dois pontos principais aqui:
Vamos listar brevemente as estruturas que a Binarly destacou no slide da Figura 2.
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
};
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
};
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
};
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.
Tomando como exemplo a entrada Microsoft-Windows-Threat-Intelligence (EtwTi), podemos verificar o parâmetro global ThreatIntProviderGuid para recuperar o GUID desse provedor.
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.
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.
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.
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.
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.
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.
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.
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.
Explorar algumas dessas entradas revela rapidamente um candidato em nt!KeInsertQueueApc.
Este é um ótimo candidato por alguns motivos:
A montagem mostra o seguinte layout.
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.
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:
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.
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.
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.
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
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.
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.
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.
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:
Reimplementei essa técnica para fins de pesquisa; porém, fiz alguns ajustes nas técnicas empregadas.
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.
É 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.
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.
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.
Saiba mais sobre o X-Force Red aqui. Agende uma consulta sem custo com o X-Force aqui.