Jump to content

Leandro Fróes

Mente Binária
  • Posts

    203
  • Joined

  • Last visited

  • Days Won

    9

Leandro Fróes last won the day on February 1 2018

Leandro Fróes had the most liked content!

Recent Profile Visitors

8,119 profile views

Leandro Fróes's Achievements

50

Reputation

  1. O treinamento Este treinamento é uma sólida introdução sobre Análise de Malware em ambientes Windows. Nele, os alunos aprendem, com a preocupação de entender o motivo de cada passo, o que é Análise de Malware, como e onde ela é aplicada, além de conceitos importantes por trás de diversas técnicas utilizadas por malwares a fim de analisá-los. Aprendem também como subverter essas técnicas utilizando engenharia reversa, adquirindo, assim, todo o conhecimento necessário para seguir seus estudos em desafios ainda mais avançados. Cada tópico do curso é acompanhado de um ou mais exercícios para fixação do conteúdo. O que você vai aprender Introdução à Análise de Malware Windows Internals 101 Triagem de Malware Monitoramento do Comportamento Introdução à criptografia em Análise de Malware Técnicas de ofuscação e desofuscação Análise de DLLs Runtime Linking Análise de Shellcodes Técnicas de Injeção O instrutor @Leandro Fróes é pesquisador de segurança e analista de malware numa das maiores empresas de software de segurança do mundo. Autor de vários artigos no portal Mente Binária, Leandro analisa, documenta e estuda técnicas de malwares diariamente há cinco anos! Também é o criador do Curso de Ghidra da Mente Binária e das ferramentas gftrace e manw. Pré-requisitos Lógica de Programação Ter conhecimentos básicos de engenharia reversa, como os adquiridos no treinamento CERO, A Arte da Engenharia Reversa ou no livro gratuito Fundamentos de Engenharia Reversa. VMware Player ou Pro instalado. Próximas turmas Em algum momento em 2024 🙂
  2. 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
  3. Leandro Fróes

    manw

    Imagina que você tá lá no Linux, macOS ou mesmo no Windows e quer saber quantos são e quais os parâmetros da função, por exemplo, CreatePipe(), da API do Windows. O que você faz? Bom, se estiver num ambiente gráfico, pode buscar na internet, mas no Linux é muito mais rápido usar o man né? Só que o man contém as funções da API do Linux, não do Windows. E agora? Pensando nisso, o @Leandro Fróes implementou o manw. Olha que maravilha: > .\manw64.exe createpipe CreatePipe function (namedpipeapi.h) - Win32 apps Exported by: Kernel32.dll Creates an anonymous pipe, and returns handles to the read and write ends of the pipe. BOOL CreatePipe( [out] PHANDLE hReadPipe, [out] PHANDLE hWritePipe, [in, optional] LPSECURITY_ATTRIBUTES lpPipeAttributes, [in] DWORD nSize ); Return value: If the function succeeds, the return value is nonzero.If the function fails, the return value is zero. Source: https://docs.microsoft.com/en-us/windows/win32/api/namedpipeapi/nf-namedpipeapi-createpipe E não para nas funções da API do Windows. É possível buscar uma estrtura por exemplo: > .\manw64.exe -s PROCESS_INFORMATION PROCESS_INFORMATION (processthreadsapi.h) - Win32 apps Contains information about a newly created process and its primary thread. It is used with the CreateProcess, CreateProcessAsUser, CreateProcessWithLogonW, or CreateProcessWithTokenW function. typedef struct _PROCESS_INFORMATION { HANDLE hProcess; HANDLE hThread; DWORD dwProcessId; DWORD dwThreadId; } PROCESS_INFORMATION, *PPROCESS_INFORMATION, *LPPROCESS_INFORMATION; Source: https://docs.microsoft.com/en-us/windows/win32/api/processthreadsapi/ns-processthreadsapi-process_information E tem muito mais. ? Código-fonte e releases para vários sistemas operacionais disponíveis no repositório do autor no Github.
  4. Hoje daremos início a uma série de artigos sobre diversos assuntos ligados a Análise de Malware. Nesta série, falaremos sobre diversas técnicas que são/podem ser utilizadas por malwares e também como podemos analisar tais técnicas numa perspectiva de engenharia reversa. A série será dividida em diferentes "tópicos" como, por exemplo, Injeção de código, bypass de X, ou até mesmo algum truque/dica legal que vale um artigo! A postagem dos artigos não irá seguir uma ordem em específico, uma vez que um artigo não dependerá do outro (mas poderão se complementar obviamente). Além disto, para cada artigo serão também apresentadas algumas dicas de análise (dentro do que sei no momento da escrita do artigo, claro) em relação à técnica em si sendo apresentada. Sem mais enrolação, bora pra o que interessa! ? Introdução à Injeção de DLLs: Injeção de DLL como um todo é uma abordagem bastante conhecida e utilizada faz uns bons anos por diversos tipos de software, incluindo malwares. Por consequência da grande utilização por criadores de malware, desenvolvedores de software de segurança (e.g. AV/EDR) implementam várias técnicas para tentar impedir tal abordagem. Hoje em dia, num ponto de vista de desenvolvimento de malware, métodos convencionais (LoadLibrary via CreateRemoteThread, por exemplo) simplesmente não são tão viáveis, uma vez que é BEM provável que o software de segurança em questão (caso haja um, claro) implemente algo para lidar com isto, seja fazendo hooking em user-mode, listando os módulos carregados no processo e analisando o binário em disco, checando permissões das páginas para ver se alguma possui ERW, e por aí vai. O ponto é que a ideia como um todo é bem manjada já. Com isto em mente, tanto criadores de malware quanto Red Teamers passaram a criar diversas abordagens (bem criativas por sinal) de se carregar uma DLL no processo alvo e iremos explorar alguns destes métodos nesta série. Reflective DLL Injection: No artigo de hoje falaremos um pouco de uma técnica chamada Reflective DLL Injection, que mesmo sendo meio antiga ainda funciona muito bem e é utilizada não só por malwares, mas também por ferramentas ofensivas como o famoso Cobalt Strike. Criada por Stephen Fewer, esta técnica utiliza o conceito de Programação Reflexiva para cumprir um único objetivo: carregar uma DLL no processo alvo deixando o mínimo de rastros possíveis, isto é, dependendo muito pouco dos recursos do sistema que costumam ser monitorados por softwares de segurança. Este objetivo é alcançado através da implementação de um "Mini loader" em uma função exportada (neste caso chamada "ReflectiveLoader") na própria DLL sendo carregada. Este loader garante que todos os requisitos mínimos exigidos pelo Windows para que a DLL carregada seja um binário válido sejam cumpridos. Antes de começar: Para a explicação ficar de certa forma mais simples e fluída vou explicá-la exatamente como ela foi implementada no código original. Para que a técnica funcione precisamos garantir que atendemos no mínimo 2 coisas: A DLL alvo deve ser carregada/injetada e seus bytes escritos no processo alvo; Todos os requisitos mínimos para tornar esta DLL um binário pronto para ser executado foram atendidos (realocação, tabela de importação, etc). Considerando que a implementação da técnica tem seu código aberto utilizaremos trechos do código original ao longo do artigo. O código é bastante comentado e eu super recomendo a leitura/estudo de tal para qualquer pessoa que queira entender a técnica nos mínimos detalhes. ? Inicialização: A forma utilizada para iniciar toda a execução da técnica no código original é através de um "injetor". Este binário é o responsável pelo primeiro requisito mínimo, isto é, obter os bytes da DLL, alocar memória no processo alvo, copiar a DLL para o espaço de memória alocado e passar a execução para a função exportada da DLL da qual garante o segundo requisito mínimo exigido. Para começar precisamos obter os bytes da DLL a ser carregada. No código fonte original, por exemplo, a DLL está escrita em disco e seus bytes são lidos da seguinte forma: hFile = CreateFileA( cpDllFile, GENERIC_READ, 0, NULL, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, NULL ); if( hFile == INVALID_HANDLE_VALUE ) BREAK_WITH_ERROR( "Failed to open the DLL file" ); dwLength = GetFileSize( hFile, NULL ); if( dwLength == INVALID_FILE_SIZE || dwLength == 0 ) BREAK_WITH_ERROR( "Failed to get the DLL file size" ); lpBuffer = HeapAlloc( GetProcessHeap(), 0, dwLength ); if( !lpBuffer ) BREAK_WITH_ERROR( "Failed to get the DLL file size" ); if( ReadFile( hFile, lpBuffer, dwLength, &dwBytesRead, NULL ) == FALSE ) BREAK_WITH_ERROR( "Failed to alloc a buffer!" ); Em seguida, precisamos obter um handle para o processo que terá a DLL carregada em seu address space. Este handle será passado para a função VirtualAllocEx posteriormente para alocar memória no processo. Para a explicação ficar mais simples, uma vez que não iremos cobrir questões de permissões, tokens, etc, neste artigo vamos assumir que o processo alvo é o processo do próprio injetor, onde seu Process ID (PID) foi obtido utilizando a função GetCurrentProcessId. Ao obtermos o Process ID, os bytes da DLL a ser carregada e seu tamanho podemos inicializar a rotina de injeção da DLL no processo alvo: dwProcessId = GetCurrentProcessId(); [...] hProcess = OpenProcess( PROCESS_CREATE_THREAD | PROCESS_QUERY_INFORMATION | PROCESS_VM_OPERATION | PROCESS_VM_WRITE | PROCESS_VM_READ, FALSE, dwProcessId ); if( !hProcess ) BREAK_WITH_ERROR( "Failed to open the target process" ); hModule = LoadRemoteLibraryR( hProcess, lpBuffer, dwLength, NULL ); if( !hModule ) BREAK_WITH_ERROR( "Failed to inject the DLL" ); DICAS DE ANÁLISE: Sempre fique atento a funções como CreateFile e OpenProcess, pois estas indicam que provavelmente o arquivo e/ou processo sendo passado como parâmetro será manipulado de alguma forma. No caso de OpenProcess o processo é indicado através do PID e podemos checar qual o nome/full path deste processo buscando pelo PID em questão utilizando diversas ferramentas como Process Explorer, Process Hacker, x64dbg, e por aí vai. Já na CreateFile o full path do arquivo/device é passado como sendo o primeiro parâmetro da função. Tenha em mente que se a DLL não for lida do disco a dica da CreateFile não funcionará, no entanto os bytes ainda assim precisam ser lidos de algum lugar, seja de uma região de memória encriptada, da seção de recursos, etc. Caso você perca a execução destas funções por qualquer motivo podemos ainda assim obter as mesmas informações (isto é, o nome do processo e o full path do arquivo) de funções como VirtualAllocEx, WriteProcessMemory e ReadFile, uma vez que as 3 recebem os handles referentes ao processo alvo e arquivo alvo, respectivamente. Injeção: Como vimos no código acima a função LoadRemoteLibraryR foi chamada passando o Process ID, os bytes da DLL lida e o tamanho do buffer que contém estes bytes da DLL. O objetivo dessa função é: Obter o endereço da função exportada que implementa o loader da nossa DLL; Alocar memória no processo alvo (no próprio injetor no nosso caso) a fim de injetar todo o conteúdo da DLL alvo a ser carregada; Escrever os bytes da DLL no processo alvo, como comentado acima; Executar a função exportada em questão de alguma forma. *Existem diversas formas de se executar funções (que por sua vez podem ser entrypoints, shellcodes, etc) no Windows, desde criando threads até via funções de callback aleatórias que vemos por aí (e vai por mim, o Windows tem MUITAS). O autor da técnica decidiu implementar 2 técnicas a fim de executar a função exportada, são elas ou via CreateRemoteThread passando o endereço da função exportada como "entrypoint" da thread (utilizada no exemplo abaixo) e outra forma via ponteiro de função, que basicamente faz a mesma coisa só que evita a criação de uma thread no processo alvo. A primeira coisa que é feita é tentar achar o endereço da função exportada responsável pelo segundo requisito. Para não poluir muito o artigo não vou colocar o código responsável por esta parte, até porque ele foge um pouco do escopo do artigo. Resumidamente o que é feito ali é o seguinte: É feito um parsing nas estruturas do PE em questão (DLL) até chegar em seu Export Directory. Com ele em mãos 2 tabelas podem ser resgatadas, uma que aponta para o endereço das funções exportadas (AddressOfFunctions) e outra que aponta para os nomes destas funções (AddressOfNames). É feito então um looping que checa o nome de cada função exportada pela DLL e então uma comparação para ver se o nome é o esperado (no nosso caso "ReflectiveLoader"). Se for o caso o RVA (Relative Virtual Address) é obtido através da tabela de endereços acima, convertido para offset e então retornado. Caso nada disso tenha feito sentido aconselho você a dar uma estudada sobre o formato PE. ? O injetor então aloca memória suficiente para toda a DLL no processo alvo e então escreve os bytes da DLL no espaço alocado. Depois disto o endereço da função exportada é calculado somando o offset obtido anteriormente com o endereço base, isto é, o valor retornado da função VirtualAllocEx. Para finalizar, o endereço da função é passado como "entrypoint" de uma thread criada no processo alvo: if( !hProcess || !lpBuffer || !dwLength ) break; // check if the library has a ReflectiveLoader... dwReflectiveLoaderOffset = GetReflectiveLoaderOffset( lpBuffer ); if( !dwReflectiveLoaderOffset ) break; // alloc memory (RWX) in the host process for the image... lpRemoteLibraryBuffer = VirtualAllocEx( hProcess, NULL, dwLength, MEM_RESERVE|MEM_COMMIT, PAGE_EXECUTE_READWRITE ); if( !lpRemoteLibraryBuffer ) break; // write the image into the host process... if( !WriteProcessMemory( hProcess, lpRemoteLibraryBuffer, lpBuffer, dwLength, NULL ) ) break; // add the offset to ReflectiveLoader() to the remote library address... lpReflectiveLoader = (LPTHREAD_START_ROUTINE)( (ULONG_PTR)lpRemoteLibraryBuffer + dwReflectiveLoaderOffset ); // create a remote thread in the host process to call the ReflectiveLoader! hThread = CreateRemoteThread( hProcess, NULL, 1024*1024, lpReflectiveLoader, lpParameter, (DWORD)NULL, &dwThreadId ); Até então todas as tarefas foram executadas pelo injetor. A partir de agora, considerando que estamos executando a função exportada da nossa DLL, tudo será feito pela DLL em si. DICA DE ANÁLISE: Funções como VirtualAlloc e VirtualAllocEx retornam o endereço base da região de memória alocada, ou seja, isto é um prato cheio para colocarmos breakpoints nesta região e observarmos como a região é manipulada (o que é escrito lá, por exemplo). É um tanto normal malwares/packers/etc não se preocuparem muito em quais permissões eles dão para as páginas quando alocam memória com VirtualAlloc, por exemplo, e na maioria das vezes utilizam ERW (Execução, Leitura, Escrita, respectivamente). Isto é muito bom tanto para análises manuais quanto de softwares de segurança. No nosso caso podemos ir na aba de "Memory Map" do x64dbg, por exemplo, e buscar por regiões com estas permissões. Bem provável que tem algo interessante lá! Tenha em mente que se as permissões forem alteradas (utilizando VirtualProtect, por exemplo) logo depois que a operação naquela região for feita ficará mais difícil aplicar esta dica. Funções como WriteProcessMemory recebem o buffer contendo os dados a serem escrito no processo como um de seus parâmetros (além de recebem o PID do processo como já mencionado previamente). Podemos ler o endereço deste buffer para checar o que está sendo escrito. Existem casos que várias chamadas à WriteProcessMemory são feitas e os dados são escritos como se fossem blocos e neste caso podemos simplesmente observar o local onde os bytes estão sendo escritos ao invés do buffer em si sendo escrito. Funções como CreateThread e CreateRemoteThread recebem 2 parâmetros importantes, são eles o endereço de uma função que será o "entrypoint" da thread sendo criada e os parâmetros desta função caso haja algum. Sempre fique de olho nestes endereços e na execução destas funções de entrada da thread as controlando com breakpoints, etc. Mini Loader: A forma mais manjada de se carregar uma DLL é utilizando a função LoadLibrary. Até aí tudo bem, no entanto, como mencionado anteriormente, a técnica apresentada neste artigo tenta ser o "mais silenciosa possível" e a utilização desta função pode complicar um pouco as coisas. Um dos problemas é que utilizar a LoadLibrary fará com que a lista de módulos carregados do seu processo seja atualizada com o módulo carregado, isto é, sua DLL, dando acesso a coisas relevantes para um software de segurança como por exemplo o full path, ImageBase e por aí vai, o que não é lá tão "silencioso". Além disso, justamente pela função LoadLibrary ser tão simples de se utilizar e poderosa geralmente ela está hookada (em userland no caso, mas falaremos disto em outro artigo) e antes mesmo do módulo ser carregado é bem provável que o Anti-Vírus, por exemplo, já tenha analisado sua DLL em disco e já tome uma ação em cima dela (impedindo o carregamento dela, por exemplo). A fim evitar estas situações esta técnica implementa um "Mini Loader", que é implementado na função exportada mencionada acima (exportada pela própria DLL que será carregada). Vou me referir a este "Mini loader" como simplesmente loader, mas tenha em mente que não estou falando do loader convencional do Windows. E sim! Todo o loader é implementado dentro da própria DLL que queremos carregar (daí o conceito de Programação Reflexiva), doideira né? A primeira ação executada pelo loader é tentar obter o endereço base da DLL em si executando a função exportada. Este endereço será necessário pois será utilizado para parsear as estruturas do formato PE necessárias para o loader. No entanto, para chegarmos até a base da DLL precisamos de um ponto de partida, uma vez que a função exportada não faz ideia de onde ela foi carregada. Temos várias formas de obter um endereço de referência para trabalharmos (call $0 seguido de pop <reg>, por exemplo) e a utilizada pelo autor foi criando uma função "caller" que irá servir como uma espécie de "ponte" para forçar a execução da instrução CALL. Esta instrução por padrão coloca o endereço da próxima instrução a ser executada depois da CALL na pilha, nos permitindo obter este endereço de alguma forma. Este endereço na pilha é então retornado executando a função _ReturnAddress, chamada dentro deste "caller". Com um endereço de "referência" em mãos, nós iremos "andar para trás" (literalmente decrementar) o endereço de referência até achar as strings "MZ" (assinatura do DOS) e "PE\x0\x0" (assinatura PE), indicando que atingimos o endereço base da DLL: #pragma intrinsic( _ReturnAddress ) __declspec(noinline) ULONG_PTR caller( VOID ) { return (ULONG_PTR)_ReturnAddress(); } [...] // we will start searching backwards from our callers return address. uiLibraryAddress = caller(); // loop through memory backwards searching for our images base address // we dont need SEH style search as we shouldnt generate any access violations with this while( TRUE ) { if( ((PIMAGE_DOS_HEADER)uiLibraryAddress)->e_magic == IMAGE_DOS_SIGNATURE ) { uiHeaderValue = ((PIMAGE_DOS_HEADER)uiLibraryAddress)->e_lfanew; // some x64 dll's can trigger a bogus signature (IMAGE_DOS_SIGNATURE == 'POP r10'), // we sanity check the e_lfanew with an upper threshold value of 1024 to avoid problems. if( uiHeaderValue >= sizeof(IMAGE_DOS_HEADER) && uiHeaderValue < 1024 ) { uiHeaderValue += uiLibraryAddress; // break if we have found a valid MZ/PE header if( ((PIMAGE_NT_HEADERS)uiHeaderValue)->Signature == IMAGE_NT_SIGNATURE ) break; } } uiLibraryAddress--; } Um ponto falho aqui seria o caso do compilador otimizar a "caller" e tornar ela inline. Neste caso não haveria a execução da instrução CALL e toda a operação falharia! Depois disto os passos executados são os seguintes: Obtém a lista de módulos carregados no processo lendo a lista "InMemoryOrderModuleList" dentro do PEB* (Process Environment Block) do processo; Tenta achar o módulo da kernel32.dll na lista de módulos carregados; Obtém o ImageBase (endereço base) do módulo da kernel32 para fazer o parsing das estruturas do módulo (e.g. Export Table); Parseia a Export Table da kernel32 a fim de achar o endereço das funções LoadLibraryA, GetProcAddress e VirtualAlloc. Estas funções serão utilizas para construir a tabela de importação da nossa DLL; Faz a mesma coisa com o módulo da ntdll, mas agora buscando pelo endereço da função NtFlushInstructionCache. *Toda aplicação em user-mode no Windows contém uma estrutura chamada de Process Environment Block. Ela é uma das raras estruturas que é "de sistema" e que é exposta em user-mode. O motivo por trás dessa "exposição" é que ela contém informações relevantes utilizadas por componentes como o loader do Windows, Heap Manager, susbsystem DLLs, etc e se estes componentes tivessem que acessar tais informações via syscalls seria muito caro num ponto de vista de performance. No nosso caso a informação relevante é a lista de módulos carregados no nosso processo e suas informações. O código abaixo demonstra como os passos descritos acima são executados: // get the Process Enviroment Block #ifdef WIN_X64 uiBaseAddress = __readgsqword( 0x60 ); #else #ifdef WIN_X86 uiBaseAddress = __readfsdword( 0x30 ); #else WIN_ARM uiBaseAddress = *(DWORD *)( (BYTE *)_MoveFromCoprocessor( 15, 0, 13, 0, 2 ) + 0x30 ); #endif #endif [...] uiBaseAddress = (ULONG_PTR)((_PPEB)uiBaseAddress)->pLdr; // get the first entry of the InMemoryOrder module list uiValueA = (ULONG_PTR)((PPEB_LDR_DATA)uiBaseAddress)->InMemoryOrderModuleList.Flink; while( uiValueA ) { // get pointer to current modules name (unicode string) uiValueB = (ULONG_PTR)((PLDR_DATA_TABLE_ENTRY)uiValueA)->BaseDllName.pBuffer; // set bCounter to the length for the loop usCounter = ((PLDR_DATA_TABLE_ENTRY)uiValueA)->BaseDllName.Length; // clear uiValueC which will store the hash of the module name uiValueC = 0; // compute the hash of the module name... do { uiValueC = ror( (DWORD)uiValueC ); // normalize to uppercase if the madule name is in lowercase if( *((BYTE *)uiValueB) >= 'a' ) uiValueC += *((BYTE *)uiValueB) - 0x20; else uiValueC += *((BYTE *)uiValueB); uiValueB++; } while( --usCounter ); // compare the hash with that of kernel32.dll if( (DWORD)uiValueC == KERNEL32DLL_HASH ) { // get this modules base address uiBaseAddress = (ULONG_PTR)((PLDR_DATA_TABLE_ENTRY)uiValueA)->DllBase; // get the VA of the modules NT Header uiExportDir = uiBaseAddress + ((PIMAGE_DOS_HEADER)uiBaseAddress)->e_lfanew; // uiNameArray = the address of the modules export directory entry uiNameArray = (ULONG_PTR)&((PIMAGE_NT_HEADERS)uiExportDir)->OptionalHeader.DataDirectory[ IMAGE_DIRECTORY_ENTRY_EXPORT ]; // get the VA of the export directory uiExportDir = ( uiBaseAddress + ((PIMAGE_DATA_DIRECTORY)uiNameArray)->VirtualAddress ); // get the VA for the array of name pointers uiNameArray = ( uiBaseAddress + ((PIMAGE_EXPORT_DIRECTORY )uiExportDir)->AddressOfNames ); // get the VA for the array of name ordinals uiNameOrdinals = ( uiBaseAddress + ((PIMAGE_EXPORT_DIRECTORY )uiExportDir)->AddressOfNameOrdinals ); usCounter = 3; // loop while we still have imports to find while( usCounter > 0 ) { // compute the hash values for this function name dwHashValue = hash( (char *)( uiBaseAddress + DEREF_32( uiNameArray ) ) ); // if we have found a function we want we get its virtual address if( dwHashValue == LOADLIBRARYA_HASH || dwHashValue == GETPROCADDRESS_HASH || dwHashValue == VIRTUALALLOC_HASH ) { // get the VA for the array of addresses uiAddressArray = ( uiBaseAddress + ((PIMAGE_EXPORT_DIRECTORY )uiExportDir)->AddressOfFunctions ); // use this functions name ordinal as an index into the array of name pointers uiAddressArray += ( DEREF_16( uiNameOrdinals ) * sizeof(DWORD) ); // store this functions VA if( dwHashValue == LOADLIBRARYA_HASH ) pLoadLibraryA = (LOADLIBRARYA)( uiBaseAddress + DEREF_32( uiAddressArray ) ); else if( dwHashValue == GETPROCADDRESS_HASH ) pGetProcAddress = (GETPROCADDRESS)( uiBaseAddress + DEREF_32( uiAddressArray ) ); else if( dwHashValue == VIRTUALALLOC_HASH ) pVirtualAlloc = (VIRTUALALLOC)( uiBaseAddress + DEREF_32( uiAddressArray ) ); // decrement our counter usCounter--; } // get the next exported function name uiNameArray += sizeof(DWORD); // get the next exported function name ordinal uiNameOrdinals += sizeof(WORD); } } [...Faz o mesmo para a NTDLL...] Caso você leia o código fonte pode notar que existe uma função extra sendo executada tanto para checar o nome dos módulos quanto das suas funções exportadas. Esta função é uma hashing function, utilizada basicamente pra não deixar a string em texto limpo em memória uma vez que ela só é resolvida na hora que é utilizada. Temos um vídeo no curso de Análise de Malware Online (AMO) aqui do Mente Binária que explica um pouco mais sobre este tipo de técnica. No nosso caso este hash é calculado utilizando a função _rotr. Talvez você esteja se perguntando por que diabos precisou dessa trabalheira toda só para pegar o endereço de funções que em teoria já estão disponíveis no nosso processo "host", uma vez que a kernel32 já está carregada nele e poderíamos simplesmente chamá-las, certo?! De forma superficial a resposta para esta pergunta é simples! Acontece que este loader é escrito utilizando uma abordagem que faz com que ele seja o que é chamado de Position Independent Code (ou PIC para os íntimos), que é basicamente uma abordagem que garante que independente de onde o código esteja sendo carregado (endereço) ou módulos disponíveis, ele alcançará seu objetivo uma vez que utiliza apenas estruturas e recursos que são garantidos estarem presentes no address space no qual ele está sendo executado (está aí o motivo de se buscar um endereço de referência tanto via PEB quanto via _ReturnAddress). Este tipo de abordagem é bastante utilizada em shellcodes, por exemplo, uma vez que estes não podem depender de endereços pré-definidos para executar suas tarefas. Continuando... O ponteiro obtido para a função VirtualAlloc é utilizado para alocar espaço suficiente para suportar toda a DLL (sim, de novo); Toda a DLL é escrita na região alocada, mas agora tomando os devidos cuidados copiando os headers, seções, utilizando o RVA ao invés do offset, etc; A Import Table é construída parseando o Import Directory a fim de se obter todas as DLLs e funções importadas destas DLLs utilizadas pela nossa DLL sendo carregada; A imagem da DLL em questão é realocada; O Entry Point (no caso de uma DLL a função DllMain) da nossa DLL já carregada e pronta é então executado. Como os 4 passos acima caem no mesmo ponto de terem mais a ver com o formato PE do que com a técnica em si não vou colocar o código aqui (até porque o artigo já está bem grandinho). No entanto, reforço novamente que vai ser super valioso se você ler o código para entender tudo! DICAS DE ANÁLISE: Como estes trechos de código não utilizam nenhuma função da API do Windows eles são um pouco mais "manuais" de se analisar, no entanto, a dica que posso dar é: fique de olho em registradores que contêm endereços de estruturas conhecidas e seus offsets! Em x86, por exemplo, o endereço da TEB (Thread Environment Block) é carregado em fs:[0] (em x64 fica em gs:[0]). Se deslocarmos 0x30 bytes (x86) ou 0x60 (x64) a partir do endereço base desta estrutura obtemos um ponteiro para o PEB, e a partir dela pegamos a lista de módulos carregados e por aí vai! QUASE tudo isto está documentado na MSDN, mas caso não esteja certeza que você acha no Google e/ou em algum repositório por aí. ? A mesma ideia de acompanhar offsets conhecidos se aplica ao parsing de estruturas do formato PE. O offset 0x3c a partir do endereço base do binário, por exemplo, representa o campo e_lfanew do IMAGE_DOS_HEADER. Este campo contém o offset para assinatura PE ("P", "E", 0x0, 0x0) do binário, que por consequência é o início da estrutura IMAGE_NT_HEADER. Para que seja feito o devido parsing das estruturas seguintes (Optional Header, Data Directories, etc) o binário precisa obter o endereço desta estrutura e com isto podemos utilizar tais valores como dicas para sabermos quais campos do formato PE estão sendo acessados. Execução da DLL: Neste ponto nós já temos nossa DLL mapeada em memória, carregada propriamente e pronta para ser executada! Isto tudo interagindo muito pouco com recursos clássicos de carregamento do sistema. O trecho abaixo mostra como a função DllMain é executada: uiValueA = ( uiBaseAddress + ((PIMAGE_NT_HEADERS)uiHeaderValue)->OptionalHeader.AddressOfEntryPoint ); // We must flush the instruction cache to avoid stale code being used which was updated by our relocation processing. pNtFlushInstructionCache( (HANDLE)-1, NULL, 0 ); // call our respective entry point, fudging our hInstance value #ifdef REFLECTIVEDLLINJECTION_VIA_LOADREMOTELIBRARYR // if we are injecting a DLL via LoadRemoteLibraryR we call DllMain and pass in our parameter (via the DllMain lpReserved parameter) ((DLLMAIN)uiValueA)( (HINSTANCE)uiBaseAddress, DLL_PROCESS_ATTACH, lpParameter ); #else // if we are injecting an DLL via a stub we call DllMain with no parameter ((DLLMAIN)uiValueA)( (HINSTANCE)uiBaseAddress, DLL_PROCESS_ATTACH, NULL ); #endif A partir de agora é bem comum que a DLL execute o seu verdadeiro código malicioso (através da DllMain), uma vez que toda a injeção já aconteceu e a DLL está pronta. DICA DE ANÁLISE: Sempre observe funções que recebem um ponteiro de função como parâmetro (CreateRemoteThread, CreateThreadpoolWait, etc) ou instruções CALL chamando endereços não tão comuns em execuções normais (endereços na pilha, registradores, etc). Geralmente elas indicam ou o início da execução de um shellcode ou de um binário que acabou de ser carregado/desofuscado/etc. Podemos obter todos os bytes de um módulo completo só com o endereço comentado acima (isto é, de uma região suspeita que encontramos alocada e sendo executada). Para isto basta seguirmos o endereço na aba de "Memory Map" do x64dbg, por exemplo, e fazermos um "dump" de toda a região: Lembrando que existem diversas variações para esta técnica, desde a modificação das funções sendo utilizadas (ou a não utilização delas) até a forma com que a permissão das páginas alocadas é manipulada e os bytes da DLL alvo obtidos. Nosso foco aqui foi apresentar de forma simples a técnica e seguindo rigorosamente o código fonte a fim de melhorar a experiência de quem quer aprender acompanhando o código fonte. E é isto! Espero que tenham gostado e qualquer dúvida/feedback/sugestão estou à disposição, seja por aqui ou pelo nosso Discord. Abs, Leandro
  5. Depois de muita espera a NSA anunciou oficialmente a inclusão de um debugger no Guidra na sua versão 10.0. Depois de muita discussão sobre esta possibilidade o time de desenvolvimento do framework lançou uma release beta da versão 10.0 ontem! Neste momento o debugger suporta analisar aplicações em userland e consegue debuggar tanto binários Windows quanto Linux (utilizando o gdb neste caso). Para quem quer começar logo de cara o Guidra disponibiliza um tutorial de início rápido em Help -> Guidra Functionality -> Debugger -> Getting Started: Existem várias formas de iniciar o debugger, desde clicando com o Botão direito -> Open With -> Debugger até direto da sua Project Window do Guidra clicando no ícone de "bug" debaixo de "Tool Chest", como mostrado abaixo: Uma vez que a ferramenta é inicializada você deve importar o arquivo a ser depurado para a ferramenta. Uma das formas de fazer isto é simplesmente o arrastando da Project Window. Uma fez carregado podemos ver a cara do mais novo debugger do Guidra: Algumas das funcionalidades são: debugging remoto utilizando GDB e windbg, rodar o debugger direto no programa do qual você está analizando estaticamente e tracing de memória. Além disso ele também conta com as funcionalidades básicas de um debugger como utilização de breakpoints, listagem de regiões de memória mapeadas, estados dos registradores e uma interface de linha de comando própria. Todas as funcionalidades listadas aqui possuem sua própria View, isto é, sua própria janela dentro da ferramenta: Vale lembrar que esta release está em sua versão beta e tem como objetivo principal coletar o feedback da comunidade. Caso queira dar uma testada e/ou dar um feedback pra galera do Guidra basta baixar a release clicando no botão abaixo ?.
  6. E lá vai mais uma do horsicq! No dia de hoje horsicq, criador de inúmeras ferramentas de análise incluindo o incrível Detect It Easy (DIE), lançou a primeira release do seu novo projeto, um analisador de arquivos MachO chamado XMachOViewer. Para quem já utilizou o DIE vai notar uma grande semelhança no design e usabilidade. Já aquelas que não estão familiarizados com as ferramentas do horsicq (deveriam, sério!) fiquem tranquilos, todas são bem simples e intuitívas de se usar. Ao contrário do DIE o XMachOViewer tem foco exclusivo em binários MachO. Em relação à funcionalidades a ferramenta consegue fazer tudo que o Detect It Easy faz e ainda mais, tudo isso com uma console exclusiva e mais detalhada: Dentre as funcionalidades novas temos a busca por padrões de criptografia (Base64, RSA, etc), muito útil para análise de malware, por exemplo: Name demangling (precisamos dizer o quanto isso é útil? ?) : E também uma funcionalidade de hashing (por que não, né?): Além disso, devido à natureza interativa da ferramenta ela permite você editar o arquivo diretamente, bastando apenas selecionar o campo que deseja e começar a digitar: A versão 0.01 está pronta para download e com certeza vale uma conferida:
  7. Nova release da ferramenta frida chega com diversas correções e facilidades em relação à suas dependências e a forma com que estas são manipuladas. Para quem não sabe do que se trata frida é um toolkit de engenharia reversa utilizado para instrumentar sua análise através de scripts, hookings, tracing e muito mais! Além disso o toolkit é portável e suportado em ambientes Windows, macOS, GNU/Linux, iOS, Android, e QNX. As mudanças mais importantes desta release giram em torno das dependências da ferramenta que agora estão todas atualizadas e em sua melhor versão. Com tais melhorias agora está muito mais fácil para quem quiser buildar o frida em suas versões anteriores direto do código fonte, o que aparentemente era um problema anteriormente. Além disso agora ficou muito mais fácil analisar problemas dentro das próprias dependências do frida. Imagine que uma função específica (Thread.backtrace(), por exemplo) está causando algum problema e você gostaria de depurá-la. Tendo em mente que ela pertence à biblioteca libunwind você poderia buildar o toolkit da seguinte forma: *Neste caso estamos buidando para nosso sistema local. Agora imagine que você já buidou o frida e quer apenas mexer na biblioteca em questão: A partir disto você pode continuar fazendo modificações em "deps/libunwind" (no nosso exemplo no caso) e executar uma compilação incremental executando novamente o comando para compilar: Fora toda essa nova granularidade foram aplicadas correções no suporte para iOS 14.2, suporte para size_t e ssize_t em funções nativas, suporte à inprocess injection em ambientes Windows e muito mais. Para fazer o download, é só clicar no botão abaixo, que te leva direto para a página de releases do projeto, onde você pode escolher qual versão quer baixar! ?
  8. ImHex é um editor hexadecimal gráfico multiplataforma e seu principal objetivo é ajudar na engenharia reversa e programação. O editor conta com inúmeras funcionalidades como por exemplo busca por strings, cópia de bytes em diversos formatos (C string, Python, etc), disassembling, hashing e muito mais. Além disso, o editor possibilita a criação das suas próprias templates para serem mapeadas no editor hexa (baseado em um formato de arquivo, por exemplo) através de uma linguagem de patterns: Dentre as novas funcionalidades adicionadas na nova release estão alguns itens da linguagem de script como por exemplo "alignTo", que alinha um valor com o que você determinar e "nextAfter", que pega o endereço logo depois de uma variável: Esta release também adicionou uma funcionalidade chamada Data Processor, que faz a ferramenta pré-processar os dados do arquivo sendo analisado de forma gráfica antes da análise do visualizador hexadecimal, permitindo a definição de valores e operações em regiões/bytes específicos: Lembrando que o ImHex possui suporte à plugins e não só o Data Processing, mas também outras funcionalidades podem ser estendidas do jeito que você quiser! Também foram adicionadas algumas outras funcionalidades como cores e utilização do teclado, assim como um conversor de base e habilidade de setar um endereço base no editor hexa: A release foi bem grande e com certeza merece uma olhada. Para fazer o download, é só clicar no botão abaixo, que te leva direto para a página de releases do projeto, onde você pode escolher qual versão quer baixar! ?
  9. A mais nova release do capa, framework de código aberto com foco em analisar as funcionalidades maliciosas de arquivos/shellcodes, veio com uma quantidade gigantesca de novas regras, melhorias e correções. Além de 50 novas regras também foi adicionado suporte à SMDA utilizando python3, classificação e mapeamento de TTPs (Tactics, Techniques and Procedures) em algumas das regras, scripts de exemplo utilizando capa como uma biblioteca em python dentre outras funcionalidades: Dentre as novas regras adicionadas estão criação tarefas agendadas via linha de comando, alocação de memória com permissão de leitura e escrita, captura de IP público, patching da linha de comando do processo e muito mais! Obviamente não podemos considerar a análise como verdade sempre, tendo em vista que existem funções que podem ser utilizadas por softwares legítimos, mas ainda assim a ferramenta ajuda e muito tendo em vista que o processo é todo automático e estático! Lembrando que por enquanto a ferramenta suporta apenas análise de arquivos no formato PE e a limitação de sua funcionalidade é baseada nas regras das quais o capa utiliza. Se você vive em outro planeta e não testou esta ferramenta ainda essa é a sua chance! Se você já testou/usa no seu dia a dia vale a pena atualizar e continuar utilizando?! ?
  10. A FireEye lançou nesta sexta-feira a release da versão 1.70 da flare-floss (FireEye Labs Obfuscated String Solver), ferramenta focada em identificar e desofuscar strings escondidas dentro de um arquivo, utilizando desde reconhecimento baseado em heurística até brute-forcing e emulação. Até o momento a ferramenta suporta apenas análise de binários Windows (PE). A ideia da ferramenta veio do fato de que os malwares atuais utilizam diversas técnicas diferentes para proteger strings importantes de um malware, como por exemplo URL, IP, configuração, paths, etc: Dentre os novos itens desta release está a adição de um um parâmetro para saída em JSON e correções em funcionalidades como suporte para IDA 7.4+ e no algoritmo de reconhecimento de strings. A funcionalidade de output em JSON é particularmente interessante no ponto de vista de automação, onde poderíamos usar todas as strings desofuscadas pela ferramenta como input para um script do qual se conecta com outra ferramenta, por exemplo: A ferramenta possui várias outras funcionalidades como por exemplo desofuscar strings em shellcodes e funções específicas do binário e também criação de scripts para o radare e IDA à fim de serem utilizados em seus arquivos ".r2" e ".idb":
  11. A Fireeye liberou faz pouco tempo uma ferramenta extremamente interessante de instrumentação e emulação de arquivos. Conhecida como speakeasy esta ferramenta emula arquivos dinamicamente tanto em kernel-land quanto em user-land. Sua última release adicionou novas chamadas de API em sua engine de emulação como por exemplo GetLogicalDrives e WNetGetConnection, assim como melhorias na emulação de shellcodes. Ao contrário de uma sandbox, que precisa virtualizar todo o sistema operacional, esta ferramenta emula apenas componentes específicos do Windows (chamadas de API, objetos, threads, registros, etc) a fim de entender o comportamento do arquivo executado e tentar identificar as ação relevante executadas: Além de arquivos completos podemos emular também um ambiente que será responsável pela execução de shellcodes: A ferramenta é feita em Python3 e pode ser executada tanto como uma ferramenta de linha de comando como também uma biblioteca, abrindo espaço para instrumentação/automação. Além disso, a ferramenta também pode se executada via container utilizando docker e também em qualquer ambiente em cloud, tendo em vista que não precisamos configura-la, apenas instalar e rodar. Pelo fato do sistema operacional não ser emulado por completo nem todas as chamadas de API serão suportados no ponto de vista de emulação. Com esta "limitação" em mente os desenvolvedores criaram uma forma bem prática de adicionar os hooks que estão faltando (não sendo emulados), permitindo que qualquer pessoa que tenha interesse possa mitigar este problema. Para mais informações vale dar uma olhada no REAME do projeto ?. E para aqueles que nos acompanharam semana passada quando falamos do IntelOwl vale lembrar que ele também utiliza o speakeasy!
  12. No dia 19 de Janeiro, o usuário cha5126568 abriu uma issue no github do famoso x64dbg e relatou que a última versão do packer Themida não estava sendo detectada pelo debugger, podendo resultar na execução com sucesso de um software malicioso, por exemplo. Além de relatar o ocorrido o usuário também deixou sua análise e possíveis resoluções para se evitar o bypass: Para quem não está familiarizado o ScyllaHide é um plugin do x64dbg de código aberto. Este plugin tem como objetivo impedir que técnicas de Anti-Debugging, frequentemente utilizadas não só por softwares maliciosos mas também por programas legítimos como Anti-Cheat de jogos, sejam executadas corretamente. O problema relatado foi o fato da última versão do Themida estar utilizando as funções GetForegroundWindow/NtUserGetForegroundWindow e depois GetWindowText/InternalGetWindowText à fim de descobrir o nome da janela do sistema sendo utilizada no momento da execução (neste caso, a janela do debugger em si), fazendo com que o packer identificasse o debugger e parasse de executar. Após alguns dias de discussão o usuário Mattiwatti, um dos principais desenvolvedores do plugin ScyllaHide, trouxe uma resolução para o problema relatado na issue. A solução encontrada foi fazer um hooking da função NtUserGetForegroundWindow, impedindo que a checagem do packer seja feita. Por mais que a correção tenha foco em Windows 10 ela se aplica muito bem à sistemas mais antigos, sem falar que o hook só funciona caso a janela retornada pela chamada da função retorne a janela de um debugger, garantindo maior precisão na execução. Para quem estiver interessado no código em si vale dar uma olhada no commit referente à esta nova release.
  13. Para aqueles que procuram uma forma rápida e simples de se montar uma infraestrutura de análise e pesquisa esta notícia é pra você! O IntelOwl é uma plataforma de OSINT (Open-Source Intelligence) e Threat Intelligence de código aberto, com sua última release publicada nesta manhã corrigindo bugs e adicionando novos analisadores ao seu arsenal. A ideia do framework é utilizar seus analisadores e em uma única chamada de API coletar o máximo de informações sobre um arquivo/IP/URL/domínio determinado pelo usuário. Estes analisadores vão desde serviços externos como VirusTotal e Shodan até analisadores estáticos da própria ferramenta e sandboxes customizadas por você mesmo! Dashboard principal do IntelOwl Página de scan de arquivos Com uma instalação bem simples e utilizando docker, o IntelOwl se mostra não só portável, mas também flexível, permitindo integração com serviços como Cuckoo, Yara e MISP. A tabela a seguir tenta mostrar de forma simples, porém objetiva algumas das principais vantagens e desvantagens sobre o IntelOwn: Lembrando que esta tabela está resumida e independente dela com certeza vale dar uma testada na ferramenta ? .
  14. Se você é da área de Segurança da Informação ou simplesmente tem interesse pelo assunto já deve ter notado que todo dia temos notícias de novos malwares surgindo, sejam eles malwares completamente novos ou variantes de um malware já conhecido. Com isto em mente, faz algum tempo que as empresas de segurança, inteligência e até mesmo pesquisadores independentes passaram a buscar métodos de automatizar não só a análise destes malwares, mas também a administração e armazenamento do arquivo em si, suas características e relacionamentos com outros arquivos demais entidades (domínios, campanhas, endereços IP, etc). Obviamente a análise automatizada não substitui a análise humana, mas já é uma ajuda e tanto considerando o número de malwares surgindo diariamente. Para cada uma destas necessidades descritas anteriormente existe uma ou mais ferramentas/plataformas que podem ser utilizadas para cumprir estes objetivos. Dentre elas estão plataformas de sandboxing como Hybrid-Analysis e AnyRun, ferramentas de análise estática de arquivos como o DIE (Detect It Easy), pev, yara, capa, e também repositórios de malware como o VirusShare e o Malware Bazaar. Não podemos negar que todas estas ferramentas/plataformas ajudam e muito no nosso dia a dia, mas ainda assim não conseguiríamos organizar nossas informações e centralizá-las em um único lugar de forma automática, tendo em vista que as as soluções descritas acima são isoladas e não conectam umas com as outras de forma nativa. A plataforma que chegou mais próximo de atingir as quatro exigências (isto é: análise automatizada, administração, armazenamento, relacionamento com demais entidades) foi uma plataforma chamada Virus Total, também conhecido como VT, atualmente administrado pelo Google. Virus Total O Virus Total trouxe para a comunidade uma forma simples e rápida de análise de IoCs (Indicator of Compromise) e também uma API bem simples de se utilizar para fins de automação. Dentre as diversas funcionalidades da plataforma estão inclusas análise estática, checagem de assinatura utilizando uma lista gigantesca de Anti-Virus, descrição das características gerais do IoC e comentários da comunidade. Além disso, ele também possui uma versão paga (bem cara por sinal) onde você pode fazer hunting de malwares utilizando regras de Yara, download de arquivos, buscas baseadas em histórico, visualização gráfica e uma API bem mais robusta e permissiva. É importante deixar claro que o termo IoC não se refere apenas à arquivos e seus hash, mas também à URL, domínios e IP. Ou seja, o VT realmente acaba se tornando uma opção super viável para começar qualquer tipo de investigação. O cenário atual de Segurança da Informação Com o passar do tempo não só a comunidade, mas também o mercado de Segurança da Informação no geral passou a notar que a única forma de se posicionar contra os ataques atuais é através de contribuição. Pelo mesmo motivo que gerou a necessidade de se criar formas automatizadas de análise, a contribuição se mostra cada dia mais que necessária pois ela não impõe limites, muito pelo contrário, ela dá liberdade o suficiente para você contribuir da forma que quiser. Um ótimo exemplo que mostra o exercício da contribuição e o quão valioso isto pode ser é o próprio Linux, que desde sua primeira versão foi liberado para receber contribuições e hoje é um dos maiores projetos existentes na área de tecnologia, com milhares de contribuidores ao redor do mundo. Com isto em mente, podemos notar uma desvantagem no VT: o espaço para contribuição é limitado. Desafios Como já comentado anteriormente, as principais funcionalidades são suportadas apenas na versão paga e infelizmente não são todos que podem pagar pelo valor do serviço. Um dos principais motivos dessa limitação é fato do código não ser aberto, isto é, estamos presos às funcionalidades que o time do VT disponibiliza. Se o código fosse disponível para a comunidade, resolveríamos tanto o problema monetário quanto a limitação de funcionalidades disponíveis. Uma outra porta que seria aberta no cenário descrito acima é a seguinte: Imagine que você, sua empresa, seu time ou um grupo de amigos estão com um projeto em mãos que envolve análise, classificação, categorização ou qualquer tipo de manipulação de malware. Com o código em mãos você teria liberdade de fazer a instalação da plataforma localmente ou em um servidor do qual você controla, limitando o acesso à quem você quiser e como quiser. A comunidade Tendo estes desafios em mente, a comunidade começou a criar alternativas para resolver alguns problemas encontrados no cenário atual. A ideia do artigo não é de forma alguma dizer que uma plataforma é melhor que outra ou que o Virus Total está errado em trabalhar no modelo que trabalha, muito pelo contrário, o objetivo aqui é mostrar as várias formas que temos de se chegar no mesmo objetivo. Uns mais flexíveis, outros com mais conteúdo disponível, mas todos conseguem te ajudar a chegar no mesmo lugar: Saferwall: Este é o projeto mais maduro que temos atualmente quando o assunto é análise automatizada e contribuição da comunidade. Robusto e flexível para ser instalado em diversos ambientes, o Saferwall consegue entregar informações estáticas de arquivos, detecções baseadas em assinaturas de alguns antivírus, identificações de packers e download dos arquivos submetidos anteriormente. Além disso, o Saferwall possui uma plataforma aberta e que aceita colaboração, além de disponibilizar o código para você instalar onde e como bem entender. Dentre as formas de instalação estão inclusas o minikube (indicado para ambientes de testes), em nuvem utilizando AWS e On-Premise. Freki: O projeto Freki foi criado por uma única pessoa, mas não deixa a desejar quando o assunto é funcionalidade e fácil instalação. Com possibilidade de ser instalado utilizando Docker, este projeto possui não só análise estática dos arquivos PE submetidos, mas também disponibiliza sua própria API e puxa informações do VT para garantir que não falte nada. Aleph: focando bastante na parte de inteligência, o projeto Aleph entrega para você não só informações estáticas dos arquivos submetidos, mas também análise dinâmica utilizando sandbox, visualização gráfica dos resultados e uma saída em JSON formatada para ser utilizada em backends como Elasticsearch, por exemplo. Além disso, o Aleph também consegue mapear as técnicas utilizadas pelo malware utilizando o MITRE ATT&CK Framework. Eu realmente aconselho você dar uma olhada na palestra da MBConf v3 sobre o Aleph para saber mais sobre o projeto. A tabela à seguir foi criada para facilitar a visualização das funcionalidades descritas acima. É importante deixar claro que a versão do VT utilizada para a criação da tabela é a gratuita: VirusTotal Saferwall Freki Aleph Análise Estática ✔️ ✔️ ✔️ ✔️ Análise Dinâmica X ✔️ X ✔️ Suporte à múltiplos SO ✔️ ✔️ X ✔️ Análise de IoC de rede ✔️ X X X Código Aberto X ✔️ ✔️ ✔️ Download de arquivos X ✔️ ✔️ ✔️ Instalação local X ✔️ ✔️ ✔️ Controle total do backend X ✔️ ✔️ ✔️ API ✔️ ✔️ ✔️ X Como podemos ver, todos estes projetos são de código aberto, o que permite a seus usuários livre contribuição. Caso você tenha interesse em contribuir para alguns desses projetos, aqui vai uma dica: nenhum deles possui ainda análise de URL/IP/domínio de forma isolada, isto é, independente do arquivo. Tenho certeza que uma contribuição deste tipo seria bem vinda. ? Conclusão Ajudando estes projetos nós não só melhoramos a ferramenta/plataforma em si, mas ajudamos todos que a utilizam e também construímos um sistema livre e aberto de análise, inteligência e investigação. Se você é da área ou simplesmente curte contribuir, não deixe de dar uma olhada em cada um destes projetos e, se possível, contribuir com eles. Lembrando que quando falamos de contribuição, não há limites. Pode ser um commit, uma ideia, ajuda monetária ou um simples OBRIGADO aos desenvolvedores e contribuidores por disponibilizarem projetos tão úteis para a comunidade.
  15. Faz algum tempo que ando botando mais a mão na massa na parte prática da análise de malware e nos conceitos que a envolvem. Este fato somado com minha paixão por tomar notas nos meus estudos acaba resultando na criação de alguns relatórios. Com isto em mente, decidi colocar aqui a análise do último sample no qual trabalhei, de um ransomware chamado Nephilin. Overview O Nephilin é uma variante do Nefilim, um Ransomware que acabou ficando bem conhecido no mês de fevereiro/março devido ao fato de ser uma variante do conhecido Nemty, que costumava trabalhar com operações de RaaS (Ransomware as a Service - um modelo de negócio utilizado por criminosos que facilita muito a distribuição de Ransomwares. Basicamente o criador do malware recruta pessoas para usarem o Ransomware e recebe uma parte de seus lucros, facilitando assim a distribuição e a entrada de pessoas não experientes no crime). Por mais que as formas de operação sejam diferentes, há similaridades de código entre essas três famílias de malware. Análise Estática O sample analisado foi compilado para x86 e não possui nenhuma técnica que possa dificultar nossa análise como por exemplo packers/protectors e o uso de ASLR. No entanto, este binário é assinado com um certificado válido: Olhando os imports podemos notar que a Import Directory Table possui apenas uma entrada, a da kernel32.dll, levantando a suspeita da utilização de runtime linking, ou seja, o loader não resolve o endereço das funções em tempo de carregamento, pois eles são resolvidos em tempo de execução: Podemos suspeitar ainda mais pelo fato do nome algumas DLLs estarem na lista de strings do binário, assim como o nome de algumas funções que não são exportadas pela kernel32.dll: Não vou me preocupar muito com esta parte estática da análise, tendo em vista que a ideia deste artigo é cair de cabeça em cada funcionalidade do malware, isto é, executá-lo e ir analisando seu comportamento. Análise Dinâmica A primeira função que o malware chama é a que vai criar todo o contexto de criptografia para gerar uma chave que será a chave utilizada para descriptografar a Ransom Note: Dentro desta função existem várias funções que permitem trabalhar com criptografia no Windows. A primeira função criptográfica chamada é a CryptAcquireContext, responsável por criar um handle para um key container dentro de um Cryptographic Service Provider (CSP). Após pegar o handle, o tamanho de uma string é calculado para posteriormente se criar e retornar um Hash Object dentro do CSP criado anteriormente. Esta string tem 66 bytes de tamanho (0x42 em hexa) e é uma string lotada de "a". A função CryptCreateHash cria o Hash Object especificando o tipo como SHA1 e, logo depois, a função CryptHashData tira o hash do que está dentro de um buffer de tamanho 66 bytes sendo passado como parâmetro: Por fim, o SHA1 gerado é derivado, para a geração de uma chave RC4. Como podemos ver quase todas as funções estão sendo importadas dinamicamente. ? O que acontece após a função que gera esta chave RC4 é a criação de um Mutex com o nome "sofos delaet sosos": Em seguida, uma string em base64 aparece e é decodada com a função CryptStringToBinary, resultando em uma chave pública RSA: "BgIAAACkAABSU0ExAAgAAAEAAQDNFw18bUF1x32DZaZt4gnQtAnv5XH60d9B6UgIbVfRdHPeyEljZLKlGBKFPTsh+8xsDHe/9vynuOlnuPt91grReMAwcTDVkxBh/PDkf3Jq0bnFgZAWbgMvGX6lApXTDcTArf4US63VI3z8YPyDNJwEvBEWI13ywob8ECLsrD/C6BPkYG0mBU1ccixzOgkgad0iDvwS/C8iyW1Mi0PCoBa+3TCTVwt0Zpy/HceV5U7SevG7RRN5HrErv54Ihg6kTPPhdxkYdO+CUND19aLqh8MAVLRuP5hR6b6r7cjBNAW2+USaaMyT/llNXdPdySbatLlH6Mau4z1eqzYc7hMB2f+6" Há depois uma tentativa de pegar um handle para um CSP onde o key container tem o nome "rsa session" e, sem sucesso, o binário cria um novo chamado "skr skr skr": Agora a chave pública decodada anteriormente será importada para o contexto "skr skr skr": Ações padrão da maioria dos ransomwares incluem desabilitar a checagem de erros durante o boot, desabilitar o modo de recuperação, deletar backups, etc. O Nephlin faz isso através de uma chamada à função ShellExecuteA() passando uma linha com o cmd.exe como parâmetro: "C:\\asdfgsdgasd\\..\\Windows\\asdgagsahsfahfhasahfsd\\..\\System32\\cmd.exe" "/c bcdedit /set {default} bootstatuspolicy ignoreallfailures & bcdedit /set {default} recoveryenabled no & wbadmin delete catalog -quiet & wmic shadowcopy delete" Aqui foi utilizada uma abordagem um tanto curiosa, tendo em vista que o malware considera diretórios que provavelmente não existirão no sistema de arquivos da vítima ("asdgagsahsfahfhasahfsd", por exemplo) e sobe um nível no filesystem utilizando ".." para acessar de fato o que importa, ou seja, o caminho real seria simplesmente "C:\Windows\System32\cmd.exe" ? Neste momento acontece uma checagem em relação à como o malware foi executado na linha de comando. Se foi passado algum parâmetro, ele checa pra ver se é um arquivo ou diretório. Se for um arquivo, ele chama direto a função que encripta. Caso seja um diretório, ele chama a função que checa uma lista de exclusão dos tipos de arquivos que ele não quer encriptar e esta função chama então a função que encripta. É interessante notar que com essas checagens o ransomware pode ser usado em diversos cenários e não simplesmente para encriptar o sistema completamente. Ex: para manualmente testar se ele está funcionando (antes da possível invasão), ser executado manualmente após a invasão visando diretórios/arquivos específicos, etc: Se nenhum parâmetro for passado via linha de comando, o binário começa a mapear os drives que estão no sistema e pegar seus tipos, buscando especificamente por drives fixos, removíveis (pen drives, etc) e mapeamentos de rede: Após pegar o drive o seu nome é concatenado com a string "NEPHILIN-DECRYPT.txt", à fim de criar a Ransom Note na raiz do Drive em questão: Após a chamada à CreateFile, podemos ver a Ransom Note sendo criada, mas vazia por enquanto: Antes do conteúdo ser de fato escrito no arquivo, ele precisa ser decodado, tendo em vista que é uma string em base64 (sim, outra string em base64): Abaixo está o buffer que contém o conteúdo da Ransom Note em base64: Após decodar o base64 o buffer aparenta estar encriptado, e de fato está: A função CryptDecrypt utiliza a chave RC4 gerada anteriormente para decriptar o conteúdo do buffer em questão. Podemos ver o conteúdo em clear text após a execução da função: Por fim podemos ver a função WriteFile, que irá escrever o conteúdo no arquivo "NEPHILIN-DECRYPT.txt" criado anteriormente: Agora que a Ransom Note foi criada, o processo de criptografia começa. O meio utilizado é através da criação de outra thread, isto é, para cada drive encontrado, uma nova thread é criada. A função CreateThread recebe como parâmetro para indicar seu início o endereço de uma função, que por sua vez chama a função que checa a lista de exclusão e depois começa a criptografia. Além disso, o nome do drive escolhido no momento é passado como parâmetro para esta função. Esta lista de exclusão é basicamente um lista que contém nomes de arquivos, diretórios e extensões das quais o malware não irá encriptar. Para cada arquivo encontrado o malware irá comparar com as especificações desta lista e, caso não bata, a função de criptografia será chamada: Criptografia A criptografia pode começar de 3 formas diferentes, como mencionado anteriormente: passando um arquivo como parâmetro pela linha de comando, passando um diretório ou mapeando os drives e criando threads. Um trecho da função que faz as devidas checagens pode ser observada abaixo: Se o arquivo checado não estiver na lista de exclusão, a função de criptografia é chamada: O processo de criptografia se inicia com a abertura do arquivo em questão e a obtenção do seu tamanho. Depois disso, há duas chamadas para a função SystemFunction036 para gerar números aleatórios. Basicamente esta função é um alias para a função RtlGenRandom, que recebe como parâmetro um buffer e o tamanho do número aleatório que você quer gerar. O tamanho escolhido são 16 bytes (0x10): Tendo 2 buffers de 10 bytes de tamanho cada, com os devidos números aleatórios gerados anteriormente, há duas chamadas à CryptEncrypt, uma para cada buffer. Aqui a chave pública RSA é utilizada para encriptar o buffer em questão, resultando em outros dois buffers de 256 bytes cada. O conjunto de funções a seguir faz a mesma operação, mas apontando para lugares diferentes. A função SetFilePointerEx é utilizada para apontar para o fim do arquivo (baseando-se no tamanho obtido anteriormente) e depois a função WriteFile é utilizada para escrever os 256 bytes encriptados lá. A próxima chamada à SetFilePointerEx agora aponta para o fim do arquivo + 256 bytes e então escreve o segundo buffer encriptado onde o ponteiro está apontando. Neste momento as checagens de tamanho de arquivo começam, assim como as chamadas de função e loops que envolvem a criptografia. A primeira checagem feita é se o arquivo é maior que 64MB e, caso seja, as funções que criptografam o arquivo começam a ser chamadas de 125KB em 125KB. Caso o arquivo seja menor há uma outra checagem para ver se ele é menor que 1.2MB e caso ele não seja as funções de criptografia rodam em cima de 600KB apenas e finalizam. Caso o arquivo seja menor que 1.2MB ele é encriptado "de uma vez" e depois finaliza. Para cada arquivo é gerada uma chave randômica com a função SystemFunction036 e depois esta é encriptada com a chave RSA pública. Esta abordagem dificulta bastante a criação de um decryptor, tendo em vista que a chave será sempre aleatória. Por outro lado, se tivemos a chave RSA privada em mãos a aleatoriedade não teria efeito nenhum pois para cada arquivo teríamos a chave responsável pela sua criptografia. Por fim a extensão ".NEPHILIN" é adicionada ao arquivo aberto: Uma coisa importante a se notar é que se a criptografia foi executada para um arquivo ou diretório específico tanto a Ransom Note quanto o wallpaper do Ransomware não são criados. Podemos observar que as funções de mapeamento de drives (que contém a criação da Ransom Note) e criação do papel de parede são ignoradas devido ao salto incondicional JMP: E por fim... Considerando ainda que não foram especificados arquivos e diretórios, a função responsável por criar a imagem do papel de parede é chamada. Há várias funções aqui e estas utilizam funções gráficas do Windows para editar a imagem em questão: Uma das funções chamadas nesta função responsável por criar a imagem é justamente a função de decoda o base64 da Ransom Note, pois o que é escrito no papel de parede é a mesma coisa da Ransom Note. Após várias funções gráficas para preparar a imagem o arquivo é finalmente criado em %TEMP%, com nome god.jpg e seu conteúdo é escrito no arquivo: Após configurar a imagem como papel de parede, o malware chama sua última função, que é responsável por fechar todos os handles e contextos de criptografia ainda pendentes: Depois disso, o processo simplesmente sai retornando 0. Lista de exclusão: NEPHILIN-DECRYPT.txt $RECYCLE.BIN NTDETECT.COM MSDOS.SYS IO.SYS boot.ini AUTOEXEC.BAT ntuser.dat desktop.ini CONFIG.SYS BOOTSECT.BAK program files program files (x86) windows ntldr RECYCLER bootmgr programdata appdata .dll .NEPHILIM .exe .log .cab .cmd .com .cpl .ini .url .ttf .mp3 .pif .mp4 .msi .lnk Espero que o estudo desta análise seja proveitoso assim como foi para mim e qualquer dúvida/feedback estou à disposição! Abraços!
×
×
  • Create New...