Introdução
No artigo de hoje falaremos sobre uma das técnicas mais utilizadas por malwares: a técnica de “Runtime Linking”. Como as aulas 19 e 20 do nosso curso do AMO já deram uma introdução bem legal sobre o assunto, esse artigo será um complemento e nosso foco será em uma forma mais “crua” e “escondida” de se utilizar/implementar esta técnica que é sem utilizar função alguma da API do Windows.
Lembrando que assim como a maioria das técnicas apresentadas nessa série de artigos, a Runtime Linking também pode ser utilizada de forma legítima, o que de fato é bem comum.
Definição
Tanto a definição quanto a implementação desta técnica acabam sendo tão simples quanto o seu nome. A técnica se resume basicamente em resolver o endereço de uma ou mais funções em tempo de execução. Para criadores de malware, as vantagens de se utilizar esta técnica são duas: evitar que componentes como a tabela de importação deem alguma ideia do que o binário possa fazer e tornar a análise do malware mais difícil.
Até aí beleza, isso já foi explicado no curso do AMO, certo? Relembrando um pouco do que foi falado lá no AMO: a função LoadLibrary é utilizada para carregar uma DLL no espaço de endereço de um processo e a função GetProcAddress é utilizada para pegar o endereço de uma função exportada por uma DLL carregada.
Considerando binários linkados dinamicamente, o que é super comum no Windows, se essas duas funções forem chamadas diretamente por tal binário, elas estarão presentes em sua tabela de importação e, consequentemente, poderão levantar alguma suspeita em relação à técnica em si sendo utilizada.
Para que estas funções não fiquem tão expostas na tabela de importação, elas não podem ser chamadas diretamente via API do Windows, ou seja, o endereço dessas funções precisa ser obtido de alguma forma em tempo de execução a fim de evitar que o loader preencha os endereços destas funções na tabela de importação (IAT) do binário em tempo de carregamento.
Até o fim deste artigo veremos que na verdade não precisamos do endereço da função GetProcAddress e às vezes nem mesmo da LoadLibrary, mas vamos ignorar isto por agora.
Process Environment Block (PEB)
Durante a criação de um processo no Windows, quando ainda em kernel-mode, é criada uma estrutura chamada Process Environment Block, também conhecida como PEB. Por mais que esta estrutura seja criada dentro na system call NtCreateUserProcess (mais especificamente na função MmCreatePeb) e possua informações importantes relacionadas ao processo como um todo, ela é uma das únicas estruturas consideradas “de sistema” que é exposta e disponível em user-mode. Esta exposição em user-mode é devido ao fato que tais informações importantes são utilizadas por diversos componentes que residem em user-mode, tais como o Heap Manager. Se estes componentes tivessem que acessar tais informações via system calls, seria muito "caro" do ponto de vista de performance.
Em user-mode, o endereço desta estrutura pode ser obtido através de um campo chamado ProcessEnvironmentBlock dentro de uma outra estrutura chamada Thread Environment Block (TEB), que possui seu endereço carregado por padrão, para cada thread, no registrador FS em x86 ou GS em x64. Não vamos entrar em detalhes sobre o que é a TEB neste artigo, mas é importante sabermos que no offset 0x30 a partir do endereço no registrador FS (x86) ou 0x60 do registrador GS (x64) obtemos um ponteiro para a PEB.
Abaixo estão as definições de ambas as estruturas utilizando o WinDbg em um binário compilado para 64 bits. Não me preocupei em colocar toda a saída do comando aqui, uma vez que são estruturas bem grandes e os campos que nos interessam estão no começo das estruturas:
0:010> dt _TEB +0x000 NtTib : _NT_TIB +0x038 EnvironmentPointer : Ptr64 Void +0x040 ClientId : _CLIENT_ID +0x050 ActiveRpcHandle : Ptr64 Void +0x058 ThreadLocalStoragePointer : Ptr64 Void +0x060 ProcessEnvironmentBlock : Ptr64 _PEB +0x068 LastErrorValue : Uint4B +0x06c CountOfOwnedCriticalSections : Uint4B +0x070 CsrClientThread : Ptr64 Void +0x078 Win32ThreadInfo : Ptr64 Void +0x080 User32Reserved : [26] Uint4B +0x0e8 UserReserved : [5] Uint4B +0x100 WOW32Reserved : Ptr64 Void [...]
0:010> dt _PEB +0x000 InheritedAddressSpace : UChar +0x001 ReadImageFileExecOptions : UChar +0x002 BeingDebugged : UChar +0x003 BitField : UChar +0x003 ImageUsesLargePages : Pos 0, 1 Bit +0x003 IsProtectedProcess : Pos 1, 1 Bit +0x003 IsImageDynamicallyRelocated : Pos 2, 1 Bit +0x003 SkipPatchingUser32Forwarders : Pos 3, 1 Bit +0x003 IsPackagedProcess : Pos 4, 1 Bit +0x003 IsAppContainer : Pos 5, 1 Bit +0x003 IsProtectedProcessLight : Pos 6, 1 Bit +0x003 IsLongPathAwareProcess : Pos 7, 1 Bit +0x004 Padding0 : [4] UChar +0x008 Mutant : Ptr64 Void +0x010 ImageBaseAddress : Ptr64 Void +0x018 Ldr : Ptr64 _PEB_LDR_DATA +0x020 ProcessParameters : Ptr64 _RTL_USER_PROCESS_PARAMETERS +0x028 SubSystemData : Ptr64 Void +0x030 ProcessHeap : Ptr64 Void +0x038 FastPebLock : Ptr64 _RTL_CRITICAL_SECTION +0x040 AtlThunkSListPtr : Ptr64 _SLIST_HEADER [...]
Existem várias formas de se obter o endereço da PEB de um processo na prática, tanto programaticamente quanto utilizando alguma ferramenta. Abaixo estão listados alguns exemplos de como fazer isto:
- Utilizando as funções intrínsecas readgsqword() e readfsdword():
#if defined(_WIN64) return (PPEB)__readgsqword(0x60); #else return (PPEB)__readfsdword(0x30);
- Diretamente em Assembly:
mov rax, gs:[60h] ; x64 mov eax, fs:[30h] ; x86
- Utilizando o comando dump no x64dbg:
- Utilizando o comando !peb no WinDbg:
0:013> !peb PEB at 00000049bc0a6000 InheritedAddressSpace: No ReadImageFileExecOptions: No BeingDebugged: Yes ImageBaseAddress: 00007ff72dbf0000 NtGlobalFlag: 0 NtGlobalFlag2: 0 Ldr 00007ff8d983a4c0 Ldr.Initialized: Yes Ldr.InInitializationOrderModuleList: 000001c697902a40 . 000001c6979a7f50 Ldr.InLoadOrderModuleList: 000001c697902bb0 . 000001c6979a6fc0 Ldr.InMemoryOrderModuleList: 000001c697902bc0 . 000001c6979a6fd0 Base TimeStamp Module 7ff72dbf0000 4178aed3 Oct 22 03:55:15 2004 C:\windows\system32\notepad.exe 7ff8d96d0000 7b5414ec Jul 26 21:12:28 2035 C:\windows\SYSTEM32\ntdll.dll 7ff8d7ea0000 4e5c27cf Aug 29 20:59:11 2011 C:\windows\System32\KERNEL32.DLL 7ff8d7020000 458acb5b Dec 21 14:58:51 2006 C:\windows\System32\KERNELBASE.dll 7ff8d7d50000 af7f8e80 Apr 21 06:12:32 2063 C:\windows\System32\GDI32.dll [...]
- Utilizando a ferramenta XNTSV do Hors:
- Também podemos obter o endereço com as funções RltGetCurrentPeb() ou NtQuerySystemInformation. Enfim, você já viu que jeito tem. ?
DICA DE ANÁLISE: Sempre que você ver uma das funções citadas acima ou trechos de código que acessem os locais citados (fs:[0x30] em x86, por exemplo) fique atento e tente descobrir o que exatamente está sendo acessado.
Como já foi comentado, a PEB possui diversos campos importantes em sua estrutura. Consequentemente, estes campos são acessados com frequência por diversas funções da API do Windows. A título de curiosidade, abaixo estão alguns exemplos (em x64) de funções que consultam campos da PEB diretamente:
- IsDebuggerPresent: consulta o campo BeingDebugged para saber se o processo está sendo debuggado ou não:
- GetProcessHeap: obtém um handle para a Heap do processo em questão através do campo ProcessHeap:
- GetModuleHandle: obtém um “handle” (endereço base, neste caso) do módulo especificado. Caso o parâmetro da função seja zero (perceba a instrução test rcx, rcx a seguir), o endereço base do próprio módulo é obtido através do campo ImageBaseAddress da PEB:
Ok, a PEB tem vários campos interessantes, mas qual deles tem a ver com os endereços das funções que queremos? Vamos lá!
Loaded Modules Database
Resumidamente, no processo de carregamento de um binário PE, uma das tarefas do Loader do Windows é identificar as DLLs das quais o binário depende. Ele faz isso parseando o Import Directory do binário. Caso alguma função pertença à uma DLL que ainda não está carregada em memória, ela é mapeada no processo e suas dependências resolvidas da mesma forma explicada anteriormente: parseando a lista de imports e carregando as devidas DLLs que exportam estes imports até que todas as dependências sejam satisfeitas.
Agora pense no seguinte cenário: e se a função LoadLibrary for chamada para carregar uma DLL qualquer em tempo de execução? Como que o loader checa se a DLL em questão já foi carregada? Não seria muito performático fazer tudo de novo. Por estes e outros motivos, o loader precisa de uma forma de controlar o que já foi carregado no processo.
Dentro da PEB existe um campo chamado Ldr do tipo PPEB_LDR_DATA e este campo representa o que podemos chamar de “Loaded Modules Database”. Este campo possui três listas duplamente linkadas que contêm informações sobre os módulos já carregados dentro do espaço de endereço de um processo.
E por que três listas? No fim das contas todas mostram a mesma coisa, mas são organizadas de formas diferentes, sendo uma organizada de acordo com a ordem de carregamento (InLoadOrderModuleList), outra de acordo com o endereço que o módulo foi mapeado (InMemoryOrderModuleList) e outra via ordem de inicialização (InInitializationOrderModuleList). Segue a estrutura PPEB_LDR_DATA, de acordo com o WinDbg:
0:010> dt _PEB_LDR_DATA ntdll!_PEB_LDR_DATA +0x000 Length : Uint4B +0x004 Initialized : UChar +0x008 SsHandle : Ptr64 Void +0x010 InLoadOrderModuleList : _LIST_ENTRY +0x020 InMemoryOrderModuleList : _LIST_ENTRY +0x030 InInitializationOrderModuleList : _LIST_ENTRY +0x040 EntryInProgress : Ptr64 Void +0x048 ShutdownInProgress : UChar +0x050 ShutdownThreadId : Ptr64 Void [...]
Talvez você tenha notado que as três listas mencionadas anteriormente são do tipo LIST_ENTRY e seguem o seguinte formato:
typedef struct _LIST_ENTRY { struct _LIST_ENTRY *Flink; struct _LIST_ENTRY *Blink; } LIST_ENTRY, *PLIST_ENTRY, PRLIST_ENTRY;
A sacada aqui é que estas estruturas estão contidas dentro de uma estrutura maior chamada LDR_DATA_TABLE_ENTRY (apresentada com mais detalhes em breve). Dentro de cada LDR_DATA_TABLE_ENTRY há um campo também do tipo LIST_ENTRY para cada uma das três listas, onde o campo Blink aponta para a entrada anterior da lista e o campo Flink para a entrada posterior. Quando a entrada atual é igual à primeira entrada, atingimos o fim da lista.
*Se você não entendeu muito bem, vale dar uma lida sobre listas duplamente ligadas e na documentação da estrutura.
Durante o processo de carregamento, para cada DLL mapeada, o loader adiciona uma entrada do tipo LDR_DATA_TABLE_ENTRY na lista. Quando um módulo é “descarregado”, esta entrada é removida.
*Essa inserção e remoção se aplica também quando chamamos funções como LoadLibrary e FreeLibrary, por exemplo.
Abaixo são alguns dos campos presentes em cada uma destas entradas do tipo LDR_DATA_TABLE_ENTRY:
0:010> dt _LDR_DATA_TABLE_ENTRY ntdll!_LDR_DATA_TABLE_ENTRY +0x000 InLoadOrderLinks : _LIST_ENTRY +0x010 InMemoryOrderLinks : _LIST_ENTRY +0x020 InInitializationOrderLinks : _LIST_ENTRY +0x030 DllBase : Ptr64 Void +0x038 EntryPoint : Ptr64 Void +0x040 SizeOfImage : Uint4B +0x048 FullDllName : _UNICODE_STRING +0x058 BaseDllName : _UNICODE_STRING +0x068 FlagGroup : [4] UChar +0x068 Flags : Uint4B +0x068 PackagedBinary : Pos 0, 1 Bit +0x068 MarkedForRemoval : Pos 1, 1 Bit +0x068 ImageDll : Pos 2, 1 Bit +0x068 LoadNotificationsSent : Pos 3, 1 Bit +0x068 TelemetryEntryProcessed : Pos 4, 1 Bit +0x068 ProcessStaticImport : Pos 5, 1 Bit [...]
Como podemos ver, bastante informação é exposta, incluindo o nome do módulo, o endereço base e o entry point.
*Em x86 o campo Ldr fica 0xC bytes de distância do endereço base da PEB. Já x64, essa distância é de 0x18 bytes.
Agora que sabemos como o loader controla os módulos carregados e como acessar esta informação, o que nos falta para obter o endereço base dos módulos e suas funções exportadas é codar!
Implementação
Uma vez que sabemos que é possível obter o endereço base e o nome dos módulos carregados de um processo de forma estável, podemos percorrer esta lista de módulos a fim de obter o endereço base dos módulos que queremos. O trecho de código abaixo é uma demonstração de como poderíamos percorrer essa lista e printar todos os nomes e endereços base dos módulos mapeados dentro do nosso processo:
inline PPEB get_peb() { #if defined(_WIN64) return (PPEB)__readgsqword(0x60); #else return (PPEB)__readfsdword(0x30); } int main() { PPEB peb = get_peb(); PLDR_DATA_TABLE_ENTRY current_module = NULL; PLIST_ENTRY current_entry = peb->Ldr->InLoadOrderModuleList.Flink; while (current_entry != &peb->Ldr->InLoadOrderModuleList && current_entry != NULL){ current_module = CONTAINING_RECORD(current_entry, LDR_DATA_TABLE_ENTRY, InLoadOrderLinks); printf("Module Name: %ls\n", current_module->BaseDllName.Buffer); printf("Module Image Base Address: 0x%p\n", current_module->DllBase); current_entry = current_entry->Flink; } }
Com a devida checagem de qual módulo estamos interessados e com seu endereço base em mãos, podemos parsear o módulo como se fosse um binário PE (até porque ele é) e então parsear sua tabela de exportação. Nesta tabela estão localizados todos os nomes/ordinais e endereços das funções que um módulo exporta (já viu onde isso pode dar, né?).
Analisando a tabela de exportação da kernel32.dll, por exemplo, podemos obter o endereço da função LoadLibrary em tempo de execução. Ao carregarmos o módulo desejado com a LoadLibrary, podemos parsear a tabela de exportação do módulo e obter a função exportada que quisermos. E não! Não precisamos da GetProcAddress uma vez que já vamos ter acesso ao endereço base de todos os módulos que precisamos e isto é o suficiente para chegarmos até a tabela de exportação.
Dependendo das funções nas quais você tem interesse, nem da LoadLibrary você vai precisar, uma vez que, por padrão, é bem provável que módulos comuns como o kernel32.dll, já tenham sido carregados pelo loader no seu processo.
*Caso você queira garantir que um módulo específico seja mapeado no seu processo, você pode importar no seu código qualquer função aleatória exportada por tal módulo. Desta forma, você irá forçar o loader a mapeá-lo em tempo de carregamento.
DICAS DE ANÁLISE:
- Não confie 100% na PEB. Quando falo em estabilidade, estou me referindo ao fato dela estar sempre presente na memória do processo e não que seus campos refletem a realidade sempre. Qualquer um que tenha acesso à memória do processo (o próprio malware rodando, por exemplo) pode alterar os campos da PEB.
- Módulos injetados via Reflective Injection, por exemplo, burlam esta adição na lista de módulos feita pelo loader, como já comentado no nosso artigo anterior.
- Nunca assuma que um software não utiliza uma determinada função só porque ela não está presente na tabela de importação. Já vimos que isto está bem longe de ser verdade.
- Colocar breakpoints em funções como LoadLibrary e GetProcAddress, mesmo quando um software utiliza Runtime Linking é uma alternativa interessante. No entanto, não ache que essas funções são sempre necessárias. A ntdll.dll no fim das contas é tudo que você precisa (até porque a LoadLibrary não é quem faz o verdadeiro trabalho, certo? ?). Lembrando que a ntdll.dll é sempre o primeiro módulo a ser mapeado em um processo e está sempre presente em aplicações em user-mode.
Vou deixar a implementação completa como desafio para você que está lendo. Vale também jogar seu binário em um debugger e analisá-lo, passo a passo, para fixar o que foi aprendido neste artigo.
Considerações e dicas finais
Uma das grandes vantagens de se utilizar a PEB para efetuar diversas atividades é a garantia de que ela sempre estará presente na memória de um processo. Se você leu o artigo anterior desta série, deve lembrar sobre Position Independent Code e o quanto este tipo de estabilidade ajuda em abordagens assim.
Em malwares, geralmente a técnica de Runtime Linking é implementada dentro de uma função que retorna o endereço da função desejada. Para estes casos, a forma mais rápida de se descobrir qual função está sendo obtida no momento é debuggar o binário, dar um Step Over sobre a função que obtém o endereço e olhar o valor de retorno (considerando que você não quer investir seu tempo analisando a função que parseia a PEB).
No entanto, não espere que isto seja sempre verdade e que a implementação seja tão direta, uma vez que muitas técnicas podem ser utilizadas para tornar a análise mais complexa. Algumas destas técnicas são:
- Hashing Functions para esconder o nome dos módulos e funções sendo buscadas.
- Inserção de junk code (trechos de código totalmente irrelevantes) para tornar a análise mais chata e complexa.
- Execução da função desejada dentro da própria função que parseia a PEB.
- Inserção dos endereços obtidos em uma tabela similar à Import Address Table (IAT), porém criada e mantida pelo código do malware ou em variáveis globais que serão utilizadas posteriormente.
Nos casos 1 e 2, se o endereço da função for retornado pela função que parseia a PEB, independentemente da técnica implementada dentro dela, o endereço da função será retornado e fica fácil identificá-lo. Já para os casos 3 e 4, nenhum endereço será retornado e você precisará analisar a função de parsing da PEB para saber onde estes endereços de funções estão sendo resolvidos.
A chave para identificar a técnica de Runtime Linking é ficar atento a padrões, pois por mais que um malware possa implementar técnica X ou Y para esconder a técnica, a PEB ainda precisa ser parseada e a lista de módulos carregados ainda precisa ser obtida.
O trecho abaixo é parte de um shellcode gerado pelo framework Cobalt Strike. Este shellcode implementa a técnica de Runtime Linking e utiliza ROR13 como sua hashing function. Note como os padrões de acesso a PEB e a export table dos módulos estão bem claros independente do resto:
A imagem abaixo é a visão tanto do código decompilado quanto disassemblado do Ransomware Conti. O padrão a ser notado aqui é o mais comum entre malwares: implementação de uma função que cuida do hashing, do parsing da PEB, e então retorna o endereço da função desejada para ser utilizado posteriormente:
Cada caso vai ser um caso e o céu é o limite. No entanto, ao sabermos como o sistema operacional lida com tais componentes e como identificar tais padrões, fica muito mais prático lidar com possíveis variações da técnica. ?
Espero que tenham gostado do artigo e qualquer dúvida, sugestão ou feedback, é só comentar que estou à disposição.
Abs,
Leandro
- 1
- 1