It Security Criacao-de-exploits Shellcoding - Encontrando endereço da função dinamicamente. Análise da biblioteca block_api
Post
Cancel

Shellcoding - Encontrando endereço da função dinamicamente. Análise da biblioteca block_api

Introdução

Neste artigo iremos dissecar a biblioteca da Metasploit chamada Block API responsável por localizar em tempo de execução o endereço das funções dentro dos módulos carregados na aplicação.

Porém, antes de entrarmos efetivamente no assunto deste post é interessante conceituar algumas coisas: A primeira delas é sobre o termo Shellcoding.

Shellcoding é um termo muito utilizado para designar um código escrito em assembly utilizando durante o processo de exploração de binários (Windows e Linux), seja para criação de um shell reverso, bind shell como para execução de comandos, execução de uma aplicação e etc.

Em um processo de criação de shellcoding temos a possibilidade de trabalhar com 2 estratégias, a primeira delas utilizando Syscall e a segunda utilizando APIs dos subsistemas do sistema operacional.

Arquitetura Windows e Linux

De forma simplificada a imagem abaixo ilustra a arquitetura do sistema operacional Linux

Fonte: https://infoslack.com/devops/linux-101-arquitetura

Bem como temos a figura abaixo ilustrando a arquitetura do Windows

Fonte: Pavel, Y at all. Windows Internals Part 1: 1. ed. Washington: Microsoft, 2017. Pg 47

Problema do Syscall

Como observado am ambas arquiteturas (Windows e Linux) temos 2 possibilidades de realizar chamadas para o SO, a primeira delas utilizando as bibliotecas e subsistemas do sistema operacional (glibc, kernel32.dll, user32.dll e etc…), a segunda metodologia é utilizando system calls (ou também conhecida como syscall).

Em um Linux é muito comum e fácil se utilizar as syscalls pois no Linux os IDs das syscalls não se alteram com novas releases, versões e etc, além de serem amplamente documentada. Já em um ambiente Windows não existe uma documentação oficial sobre o tema e é altamente refutado a utilização, pois a cada release do SO os ids das syscalls se alteram, desta forma um shellcode não se torna confiável.

Vale a pena ressaltar que existem técnicas para identificar os IDs da syscall e utiliza-las, mas isso fica para outro artigo.

Desta forma é muito comum em um ambiente windows os shellcodings utilizarem as funções expostas diretamente pelas APIs do windows (ou também conhecidas como subsistemas) que são a Kernel32.dll, user32.dll etc…

Para um melhor aprofundamento recomendo a visualização do vídeo do Rafael Salema falando sobre o assunto Stop calling APIs! Demystifying direct syscall

Objetivo deste artigo

Como em shellcoding windows geralmente utilizamos as APIs do sistema operacional e estas APIs geralmente executam no sistema operacional com Address space layout randomization - ASLR de forma que a cada execução ou a cada reboot do sistema operacional, bem como a cada compilação da DLL tem-se um endereço diferente para as chamadas de funções.

Sendo assim o shellcode para ser confiável precisa deter um método de identificar dinamicamente o endereço de uma função.

Em nossos treinamentos ensinamos a utilizar as bibliotecas da Metasploit, chamadas Block API, para este fim. Bibliotecas disponíveis em:

A propósito eu realizei algumas otimizações para que o ASM da versão em 64 bits não tenha nullbyte e de quebra houve uma redução de tamanho. Por questões internas e comentadas no Pull Request o mesmo não foi realizado o merge, mas para quem tiver interesse segue a referencia: Pull Request #17934.

Inclusive temos um mini-treinamento disponível em nosso canal do Youtube sobre Shellcoding para 64 bits: https://www.youtube.com/watch?v=ySKEF8MHcZA utilizando essa biblioteca.

O que faremos neste artigo é entender passo a passo (dissecar) o que essa biblioteca realiza, quais estruturas, tabelas e dados da aplicação ela analisa para chegar a identificar de forma precisa o endereço exato da função dentro do Windows.

Sendo assim este artigo focará somente no sistema operacional Windows.

Conceitos e referencias complementares

Durante este estudo iremos falar de diversos assuntos e daremos ênfase/aprofundamento somente naquilo que é pertinente para o nosso estudo, sendo assim para um melhor entendimento e aprofundamento recomendo a consulta aos seguintes materiais:

Caso não tenha familiaridade com instruções assembly, ponteiros e pilha, recomendo antes da continuidade da leitura a visualização desta aula do Youtube https://www.youtube.com/watch?v=ySKEF8MHcZA pois nesta aula é apresentada diversos conceitos extremamente necessários para o entendimento deste artigo.

Instalando WinDbg

Para realizar a instalação do WinDBG faça o download do SDK do Windows 10 disponível em: https://docs.microsoft.com/en-us/windows-hardware/drivers/debugger/debugger-download-tools

Após instalado realize a configuração do local de armazenamento e download dos símbolos de debug.

Abra o WinDBG x68 e vá em File > Symbol File Path e adicione o conteúdo abaixo

1
srv\*c:\symbols\*c:\symbols\*http://msdl.microsoft.com/download/symbols  

Carregue uma aplicação qualquer em 32 bits como

1
C:\Windows\SysWOW64\notepad.exe  

Recarregue todos os simbolos

1
.reload /f  

Process Internals

Cada processo windows é representado por um bloco EPROCESS (Executive Process), o bloco EPROCESS contem uma série de apontamentos para um numero grande de outras estruturas, por exemplo ETHREADS, TEB, PED entre outras.

A Figura abaixo simplifica o diagrama das estruturas do processo e threads.


Fonte: Russinovich, M at all. Windows Internals: 5. ed. Washington: Microsoft, 2009. Pg 336

Para nosso estudo vale ressaltar uma tabela extremamente importante que é a TEB (Thread Environment Block), por compatibilidade também conhecida como TIB (Thread Information Block). A TEB pode ser utilizada para obter uma série de informações do processo sem a necessidade de realizar chamadas para as APIs Win32. Entre outras informações armazena o endereço do SEH e o endereço da tabela PEB (Process Environment Block), que por sua vez através da PEB pode-se obter acesso a IAT (Import Address Table) e muito mais.
A TEB pode pode ser acessada através do registrador de segmento FS.

Loader

No momento da inicialização do aplicativo uma série de atividades são realizadas. Na prática o loader é executado antes do código da própria aplicação de forma que o mesmo é transparente ao usuário. Dentre as atividades em que o loader é responsável iremos destacar duas que são importantes para nosso estudo:

  • Tratar a IAT (Import Address Table) da aplicação e olhar para todas as DLLs que a aplicação necessita, bem como analisar recursivamente a IAS de todas as DLLs carregadas, seguido da análise da tabela de exportação das DLLs para ter certeza que as funções desejadas estão presentes.
  • Carregar e descarregar DLLs em tempo de execução, mesmo as carregadas sobre demanda e manter a lista de todos os módulos conhecida como Módules Database ou também como LDR (Loader Data Table).

Análise da block api 32 bits

A biblioteca da Block API está disponível no github da Metasploit em https://github.com/rapid7/metasploit-framework/blob/master/external/source/shellcode/windows/x86/src/block/block_api.asm

Utilização

Antes de adentrarmos a análise do código da BlockAPI vamos a um exemplo de utilização.

Neste exemplo iremos utilizar a função ExitProcess documentada em https://docs.microsoft.com/en-us/windows/win32/api/processthreadsapi/nf-processthreadsapi-exitprocess

Tendo sua sintaxe como abaixo:

1
2
3
void ExitProcess(  
UINT uExitCode  
);  

Código C

1
2
3
4
5
6
7
8
#include <Windows.h>  
#include <stdio.h>

void main(){

ExitProcess(0);

}  

Hash da api

A block_api espera como entrada no topo da pilha o hash da função desejada seguido dos parâmetros da função.

Para o cálculo do hash da função utilizaremos uma aplicação desenvolvida por mim disponível em https://github.com/helviojunior/addrfinder

Note que o hash da função ExitProcess é 0x56A2B5F0, este hash não se altera mesmo em releases diferentes do windows.

Assembly - utilizando a block_api

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
[BITS 32]

global _start

_start:  
jmp short block_api

get_block_api:  
pop edi ; Copia o endereço da block_api no registrador edi

; Sai da aplicação sem aprentar erro  
xor eax,eax ; Zera EAX  
push eax ; Coloca na pilha o "exit code" = 0x00

; Realiza a chamada da função ExitProcess  
push 0x56A2B5F0 ; Coloca o endereço do hash função ExitProcess na pilha  
call edi ; Executa a block_api para localizar e executar a função

block_api:  
call get_block_api  
%include "../block_api.asm"  

Como podemos observar no código acima na linha 22 realizamos a inclusão do arquivo da biblioteca (exatamente o mesmo arquivo listado no link do github acima)

Utilizando a estratégia de JMP; Call; POP salvamos o endereço da primeira instrução da block_api no registrador EDI

Sendo assim podemos colocar na pilha de forma que ficará como abaixo:

  • ESP + 0x00 = 0x56A2B5F0
  • ESP + 0x04 = 0x00000000

E posteriormente executamos a block_api através da instrução call edi

Montagem e executando

Utilizaremos a aplicação shellcodetester desenvolvida por mim para a realização dos testes. Denso assim você pode realizar a instalação diretamente via PyPi com o comando pip3 install shellcodetester

Para a montagem (conversão dos mnemônico ASM para binário/hexa) utilizaremos o o ShellcodeTester (Disponível em https://github.com/helviojunior/shellcodetester)

Para a instalação do mesmo basta realizar o comando

1
pip3 install --upgrade shellcodetester

Após a instalação realize a montagem e compilação de um EXE através do comando

1
shellcodetester -asm exit.asm --break-point

Abra o Windbg e execute o arquivo gerado st-exit.exe

Agora na console do windbg digite o comando go

Análise do nosso shellcode

Antes de chegar efetivamente na biblioteca da block api nós temos algumas instruções das quais podemos colocar lado a lado com nosso código

Como o foco é na execução da própria block_api vamos até o ponto da chamada call edi

Neste momento temos no registrador EDI o endereço da block_api

E começaremos a nossa análise deste ponto

Análise da block_api

Para facilitar o processo de análise vou colocando o código da block_api conforme formos evoluindo no mesmo.

Tabelas

Como comentado anteriormente há uma série de tabelas existentes e utilizadas em nosso aplicativo, sendo assim segue um diagrama com o fluxo que realizaremos na próximas instruções

Primeiramente utilizaremos o registrador de segmento FS em seu offset 0x30 para obter o endereço relativo (offset) de memória da tabela TEB, posteriormente pegaremos de dentro da TEB em seu offset 0x0C o endereço da tabela LDR e por fim dentro da tabela LDR pegaremos o endereço de memória do primeiro elemento da array InMemoryOrderModuleList.

Termos de memória

VRA (Virtual Relative Addres)

Daqui para frente utilizaremos o termo VRA (Virtual Relative Address) este termo refere-se a um endereço de memória relativo ao Base Address (ou também conhecido como Offset), de forma que o offset de uma DLL só se altera se houver a recompilação da mesma, o que o ASLR interfere é no BaseAddress, este sim se altera a cada reboot da maquina ou a cada execução da aplicação.

VMA (Virtual Memory Address)

O VMA é igual ao VRA + BaseAddress, ou seja o endereço virtual que pode ser utilizado dentro da aplicação.

Função api_call

Segue abaixo o trecho de código da primeira função api_call

1
2
3
4
5
6
7
api_call:  
pushad ; We preserve all the registers for the caller, bar EAX and ECX.  
mov ebp, esp ; Create a new stack frame  
xor edx, edx ; Zero EDX  
mov edx, [fs:edx+0x30] ; Get a pointer to the PEB  
mov edx, [edx+0xc] ; Get PEB->Ldr  
mov edx, [edx+0x14] ; Get the first module from the InMemoryOrder module list  

pushad

Pushad é uma instrução que coloca na pilha todos os registradores, em outras palavras, salva o valor de todos os registradores na pilha. Este processo consome 20 bytes da pilha

mov ebp, esp

Copia o endereço do topo da pilha para ebp. Este processo é conhecido como prólogo de uma função, ou seja, está igualando ESP e EBP para iniciar um novo stack frame

xor edx, edx

A operação matematica XOR de um valor com ele mesmo sempre resultará em Zero, sendo assim esta instrução zera o valor do registrador EDX

mov edx, [fs:edx+0x30]

Copia o VRA da PEB para dentro do registrador EDX

Dentro do windbg podemos visualizar essa informação com o comando abaixo

0:009> dt nt!_TEB @$teb  
ntdll!_TEB  
+0x000 NtTib : _NT_TIB  
+0x01c EnvironmentPointer : (null)  
+0x020 ClientId : _CLIENT_ID  
+0x028 ActiveRpcHandle : (null)  
+0x02c ThreadLocalStoragePointer : 0x0146a988 Void  
+0x030 ProcessEnvironmentBlock : 0x010da000 _PEB  
+0x034 LastErrorValue : 0  
+0x038 CountOfOwnedCriticalSections : 0  
+0x03c CsrClientThread : (null)  
+0x040 Win32ThreadInfo : (null)  
+0x044 User32Reserved : [26] 0  
+0x0ac UserReserved : [5] 0  
+0x0c0 WOW32Reserved : 0x77c16000 Void  
+0x0c4 CurrentLocale : 0x409  
+0x0c8 FpSoftwareStatusRegister : 0  
+0x0cc ReservedForDebuggerInstrumentation : [16] (null)  
+0x10c SystemReserved1 : [26] (null)  
+0x174 PlaceholderCompatibilityMode : 0 ''  
+0x175 PlaceholderHydrationAlwaysExplicit : 0 ''  
+0x176 PlaceholderReserved : [10] ""  
+0x180 ProxiedProcessId : 0  

E confirmando a informação após a execução da instrução

mov edx, [edx+0xc]

Copia o VRA da LDR para dentro do registrador EDX

0:009> dt nt!_PEB 0x010da000  
ntdll!_PEB  
+0x000 InheritedAddressSpace : 0 ''  
+0x001 ReadImageFileExecOptions : 0 ''  
+0x002 BeingDebugged : 0x1 ''  
+0x003 BitField : 0 ''  
+0x003 ImageUsesLargePages : 0y0  
+0x003 IsProtectedProcess : 0y0  
+0x003 IsImageDynamicallyRelocated : 0y0  
+0x003 SkipPatchingUser32Forwarders : 0y0  
+0x003 IsPackagedProcess : 0y0  
+0x003 IsAppContainer : 0y0  
+0x003 IsProtectedProcessLight : 0y0  
+0x003 IsLongPathAwareProcess : 0y0  
+0x004 Mutant : 0xffffffff Void  
+0x008 ImageBaseAddress : 0x00f40000 Void  
+0x00c Ldr : 0x77d40c40 _PEB_LDR_DATA  
+0x010 ProcessParameters : 0x013d19d0 _RTL_USER_PROCESS_PARAMETERS  
+0x014 SubSystemData : (null)  
+0x018 ProcessHeap : 0x013d0000 Void  
+0x01c FastPebLock : 0x77d409e0 _RTL_CRITICAL_SECTION  
+0x020 AtlThunkSListPtr : (null)  
+0x024 IFEOKey : (null)  
+0x028 CrossProcessFlags : 9  
+0x028 ProcessInJob : 0y1  
+0x028 ProcessInitializing : 0y0  

mov edx, [edx+0x14]

Copia o VRA do primeiro elemento da array InMemoryOrderModuleList da tabela LDR para o registrador EDX.

0:009> dt _PEB_LDR_DATA 0x77d40c40  
ntdll!_PEB_LDR_DATA  
+0x000 Length : 0x30  
+0x004 Initialized : 0x1 ''  
+0x008 SsHandle : (null)  
+0x00c InLoadOrderModuleList : _LIST_ENTRY [ 0x13d32a0 - 0x140d710 ]  
+0x014 InMemoryOrderModuleList : _LIST_ENTRY [ 0x13d32a8 - 0x140d718 ]  
+0x01c InInitializationOrderModuleList : _LIST_ENTRY [ 0x13d31c8 - 0x140d670 ]  
+0x024 EntryInProgress : (null)  
+0x028 ShutdownInProgress : 0 ''  
+0x02c ShutdownThreadId : (null)  

Neste ponto temos em EDX o VRA do primeiro elemento da lista duplamente encadeada InMemoryOrderModuleList.

0:009> dt _LIST_ENTRY (0x77d40c40 + 0x14)  
ntdll!_LIST_ENTRY  
[ 0x13d32a8 - 0x140d718 ]  
+0x000 Flink : 0x013d32a8 _LIST_ENTRY [ 0x13d31c0 - 0x77d40c54 ]  
+0x004 Blink : 0x0140d718 _LIST_ENTRY [ 0x77d40c54 - 0x140d878 ]

Essa informação não parece muito útil, mas conforme podemos visualizar na documentação (https://docs.microsoft.com/en-us/windows/win32/api/winternl/ns-winternl-peb_ldr_data) a estrutura LIST_ENTRY faz parte de uma estrutura maior chamada _LDR_DATA_TABLE_ENTRY

typedef struct _LIST_ENTRY {  
struct _LIST_ENTRY \*Flink;  
struct _LIST_ENTRY \*Blink;  
} LIST_ENTRY, \*PLIST_ENTRY, \*RESTRICTED_POINTER PRLIST_ENTRY;

typedef struct _LDR_DATA_TABLE_ENTRY {  
PVOID Reserved1[2];  
LIST_ENTRY InMemoryOrderLinks;  
PVOID Reserved2[2];  
PVOID DllBase;  
PVOID EntryPoint;  
PVOID Reserved3;  
UNICODE_STRING FullDllName;  
BYTE Reserved4[8];  
PVOID Reserved5[3];  
union {  
ULONG CheckSum;  
PVOID Reserved6;  
};  
ULONG TimeDateStamp;  
} LDR_DATA_TABLE_ENTRY, \*PLDR_DATA_TABLE_ENTRY;  

Para realizar o dump da estrutura temos de subtrair 0x08 do endereço da _LIST_ENTRY com o objetivo de encontrar o início da estrutura _LDR_DATA_TABLE_ENTRY

0:009> dt _LDR_DATA_TABLE_ENTRY (013d32a8 - 0x8)  
ntdll!_LDR_DATA_TABLE_ENTRY  
+0x000 InLoadOrderLinks : _LIST_ENTRY [ 0x13d31b8 - 0x77d40c4c ]  
+0x008 InMemoryOrderLinks : _LIST_ENTRY [ 0x13d31c0 - 0x77d40c54 ]  
+0x010 InInitializationOrderLinks : _LIST_ENTRY [ 0x0 - 0x0 ]  
+0x018 DllBase : 0x00f40000 Void  
+0x01c EntryPoint : (null)  
+0x020 SizeOfImage : 0xa2000  
+0x024 FullDllName : _UNICODE_STRING "C:\Tools\ShellcodeTester\Runner.exe"  
+0x02c BaseDllName : _UNICODE_STRING "Runner.exe"  
+0x034 FlagGroup : [4] "???"  
+0x034 Flags : 0x14022c4  

Função next_mod

1
2
3
4
next_mod: ;  
mov esi, [edx+0x28] ; Get pointer to modules name (unicode string)  
movzx ecx, word [edx+0x26] ; Set ECX to the length we want to check  
xor edi, edi ; Clear EDI which will store the hash of the module name  

mov esi, [edx+0x28]

Copia o VMA do nome do módulo

0:009> du @esi  
013d1eb6 "Runner.exe"  

movzx ecx, word [edx+0x26]

Recupera o tamanho do nome do módulo, lembrando que cada caractere em unicode corresponde a 2 bytes e temos mais os 2 null bytes no final

Em nosso cenário:

  • Runner.exe = 10 Caracteres + 1 null byte
  • 11 * 2 = 22

xor edi, edi

Zera o EDI para utilizar como local de armazenamento do hash do nome do módulo

Função loop_modname

1
2
3
4
5
6
loop_modname: ;  
xor eax, eax ; Clear EAX  
lodsb ; Read in the next byte of the name  
cmp al, 'a' ; Some versions of Windows use lower case module names  
jl not_lowercase ;  
sub al, 0x20 ; If so normalise to uppercase  

xor eax, eax

Zera EAX

lodsb

Carrega o primeiro byte vindo do ESI para o registrador AL.

cmp al, ‘a’

Compara o byte recebido com o caractere ‘a’

jl not_lowercase

Antes de vermos a comparação propriamente dita vamos analisar a tabela ASCII

Observe na tabela ASCII que o alfabeto minúsculo vai do hexa-decimal 0x61 até 0x7a e o maiúsculo vai de 0x41 a 0x5a, então:

  • O minúsculo é exatamente 0x20 bytes que sua representação em maiúsculo
  • O hexa-decimal do caractere em minúsculo é maior que sua representação em maúsculo

A instrução JL (Jump Short if less) verifica se o caractere em questão é menor que o caractere ‘a’, considerando que os valores em decimal/hexa-decimal dos caracteres em maiúsculo são menores que os em minúsculo, se sim o caractere é maiúsculo, neste cenário salta para a função not_lowercase

sub al, 0x20

Caso o caractere seja minúsculo, basta subtratir 0x20 que ele se tornará maiúsculo

Função not_lowercase

Esta é uma função grande que na verdade realiza as seguintes operaçÕes:

  • Cálculo do hash do nome do módulo
  • Resgata uma série de informações do módulo (Base Address, índice na lista, tabela de exports, número de funções, tabela de nomes da funções)

Desta forma iremos analisar parte por parte dessa função (em pequenos códigos)

1
2
3
4
5
6
7
8
not_lowercase: ;  
ror edi, 0xd ; Rotate right our hash value  
add edi, eax ; Add the next byte of the name  
dec ecx  
jnz loop_modname ; Loop until we have read enough  
; We now have the module hash computed  
push edx ; Save the current position in the module list for later  
push edi ; Save the current module hash for later  

ror edi, 0xd

Rotaciona 0xd (decimal, 13) bits para a direita do valor presente no EDI (Hash Value)

add edi, eax

Adiciona o byte (resgatado do nome da função) ao valor presente no EDI e salva o resultado no próprio EDI

dec ecx

Decrementa o ECX (nosso contador)

jnz loop_modname

Jump short if not zero, verifica se o resultado da ultima operação matemática é diferente de zero, ou seja, irá saltar para a função loop_modname enquanto o ECX for maior que zero

push edx

Salva na pilha o valor de EDX que neste momento representa o índice do módulo na tabela LDR.InMemoryOrderModuleList

push edi

Salva na pilha o hash do nome do módulo atual

Função not_lowercase - parte 2

Essa fase da função irá buscar as informações das funções exportadas de dentro do módulo atual.

1
2
3
4
5
6
7
8
9
10
11
12
; Proceed to iterate the export address table,  
mov edx, [edx+0x10] ; Get this modules base address  
mov eax, [edx+0x3c] ; Get PE header  
add eax, edx ; Add the modules base address  
mov eax, [eax+0x78] ; Get export tables RVA  
test eax, eax ; Test if no export address table is present  
jz get_next_mod1 ; If no EAT present, process the next module  
add eax, edx ; Add the modules base address  
push eax ; Save the current modules EAT  
mov ecx, [eax+0x18] ; Get the number of function names  
mov ebx, [eax+0x20] ; Get the rva of the function names  
add ebx, edx ; Add the modules base address  

mov edx, [edx+0x10]

Neste momento ainda temos no EDX o endereço da estrutura _LIST_ENTRY do módulo atual, sendo assim em seu offset 0x10 tem-se o BaseAddress do módulo, desta forma esta instrução copia o BaseAddress do módulo que está sendo analisado para o Registrador EDX

Note que para realizar o parse da estrutura _LDR_DATA_TABLE_ENTRY temos de subtrair 0x08, então o Offset que aparece na imagem é 0x18, ou seja 0x10 + 0x08. Onde temos o valor 0x00f40000

Valor este que podemos confirma de mais outros 2 modos

0:009> dd @edx + 10  
013d32b8 00f40000 00000000 000a2000 00480046  
013d32c8 013d1e84 00160014 013d1eb6 014022c4  
013d32d8 0000ffff 77d40ac0 013d31f4 5f125ed8  
013d32e8 00000000 00000000 013d3350 013d3350  
013d32f8 013d3350 00000000 00000000 00000000  
013d3308 00000000 00000000 0140d099 013d4f64  
013d3318 013d38c4 00000000 00400000 00000000  
013d3328 11fb4e0f 01d79199 10078c54 00000004

0:009> lm m runner  
Browse full module list  
start end module name  
00f40000 00fe2000 Runner C (no symbols)  

Neste momento temos em EDX o BaseAddress do módulo que está sendo verificado.

Binary internals

Para facilitar o entendimento vamos adentrar nas tabelas que iremos resgatar as informações

MS-DOS PE HEader

https://www.aldeid.com/wiki/PE-Portable-executable

PE HEader

BaseAssress + 0x3c = Início do PE Header

Export Table

https://www.aldeid.com/wiki/PE-Portable-executable#Export_Table

A tabela de exports está no offset 0x78 a partir do início do PE Header. Cada módulo (Executável/DLL) conterá o seu próprio PE Header e consequentemente a sua tabela de exportação.

Função not_lowercase - parte 2 continuação

Temos na imagem o parse dos dados da DOS_HEADER

0:009> dt ntdll!_IMAGE_DOS_HEADER 00f40000  
+0x000 e_magic : 0x5a4d  
+0x002 e_cblp : 0x90  
+0x004 e_cp : 3  
+0x006 e_crlc : 0  
+0x008 e_cparhdr : 4  
+0x00a e_minalloc : 0  
+0x00c e_maxalloc : 0xffff  
+0x00e e_ss : 0  
+0x010 e_sp : 0xb8  
+0x012 e_csum : 0  
+0x014 e_ip : 0  
+0x016 e_cs : 0  
+0x018 e_lfarlc : 0x40  
+0x01a e_ovno : 0  
+0x01c e_res : [4] 0  
+0x024 e_oemid : 0  
+0x026 e_oeminfo : 0  
+0x028 e_res2 : [10] 0  
+0x03c e_lfanew : 0n128  
0:009> ? 0n128  
Evaluate expression: 128 = 00000080  

mov eax, [edx+0x3c]

Copia RVA do PE Header para o registrador EAX

Podemos observar que o EAX teve seu valor definido como 0x80, ou seja o PE Header está em Base Address + 0x80 como vemos no output abaixo

0:009> dt ntdll!_IMAGE_NT_HEADERS 00f40000 + 0x80  
+0x000 Signature : 0x4550  
+0x004 FileHeader : _IMAGE_FILE_HEADER  
+0x018 OptionalHeader : _IMAGE_OPTIONAL_HEADER  

Adicionalmente podemos observar os cabeçalhos adicionais no Offset 0x80 em relação ao PE Header

0:009> dt ntdll!_IMAGE_OPTIONAL_HEADER 00f40000 + 0x80 + 0x18  
+0x000 Magic : 0x10b  
+0x002 MajorLinkerVersion : 0x30 '0'  
+0x003 MinorLinkerVersion : 0 ''  
+0x004 SizeOfCode : 0x51e00  
+0x008 SizeOfInitializedData : 0x4b800  
+0x00c SizeOfUninitializedData : 0  
+0x010 AddressOfEntryPoint : 0x53cf2  
+0x014 BaseOfCode : 0x2000  
+0x018 BaseOfData : 0x54000  
+0x01c ImageBase : 0x400000  
+0x020 SectionAlignment : 0x2000  
+0x024 FileAlignment : 0x200  
+0x028 MajorOperatingSystemVersion : 4  
+0x02a MinorOperatingSystemVersion : 0  
+0x02c MajorImageVersion : 0  
+0x02e MinorImageVersion : 0  
+0x030 MajorSubsystemVersion : 4  
+0x032 MinorSubsystemVersion : 0  
+0x034 Win32VersionValue : 0  
+0x038 SizeOfImage : 0xa2000  
+0x03c SizeOfHeaders : 0x200  
+0x040 CheckSum : 0  
+0x044 Subsystem : 2  
+0x046 DllCharacteristics : 0x8540  
+0x048 SizeOfStackReserve : 0x100000  
+0x04c SizeOfStackCommit : 0x1000  
+0x050 SizeOfHeapReserve : 0x100000  
+0x054 SizeOfHeapCommit : 0x1000  
+0x058 LoaderFlags : 0  
+0x05c NumberOfRvaAndSizes : 0x10  
+0x060 DataDirectory : [16] _IMAGE_DATA_DIRECTORY  

Dentro dos cabeçalhos adicionais podemos encontrar que a Export table (DataDirectory) encontra-se no Offset 0x60 relativo aos cabeçalhos adicionais.

Desta forma se considerarmos que os cabeçalhos adicionais estão em 0x18 em relação ao PE Heder podemos então inferir que com relação ao PE Header a Exporta table está (0x18 + 0x60) = 0x78

add eax, edx

Adiciona o RVA com o BaseAddress do módulo atual para obter o VMA do PE Header e o salva no registrador EAX

mov eax, [eax+0x78]

Copia o RVA da tabela de exports para o registrador EAX

Como pode-se observar este é um cenário onde o módulo atual não detém nenhuma função exportada. Sendo assim iremos adicionar um breakpoint neste ponto do código para podermos executar o código até que chegue no módulo desejado. Como a função exitprocess está dentro do módulo kernel32.dll vamos executar o código até chegar neste ponto dentro do módulo kernel32.dll.

Note que agora vamos executar o comando g, e a execução segue até nosso breakpoint, posteriormente podemos inspecionar qual é o módulo que estamos tratando com o comando lm a @edx uma vez que temos em ECX o BaseAddress do módulo atual

Uma vez que chegamos a kernel32.dll, podemos continuar a verificação.

test eax, eax

Verifica se há uma tabela de exports

Existe a tabela, ou seja EAX é diferente de zero, então o JMP não vai ocorrer.

jz get_next_mod1

Jump near if 0, verifica se o resultado da ultima operação matemática foi zero, se sim, realiza o salto. De forma que verificará se não há tabela de exports salta para a função get_next_mod1, caso contrário continua para a proxima instrução

add eax, edx

Adiciona o RVA com o BaseAddress do módulo atual para obter o VMA da tabela de exportação e o salva no registrador EAX

push eax

Salva na pilha o VMA da tabela de exports do módulo atual

mov ecx, [eax+0x18]

Uma vez que temos em EAX o VMA da tabela de exporta copia o número de funções exportadas para o registrador ECX

mov ebx, [eax+0x20]

Copia o RVA do array com o nome das funções exportadas (AddressOfNames) para o registrador EBX

add ebx, edx

Adiciona o RVA com o BaseAddress do módulo atual para obter o VMA do array contendo o nome de todas as funções exportadas pelo módulo atual e o salva no registrador EBX

Neste momento temos em EBX o endereço de memória com o nome da primeira função

Função get_next_func

1
2
3
4
5
6
7
8
; Computing the module hash + function hash  
get_next_func: ;  
test ecx, ecx ; Changed from jecxz to accomodate the larger offset produced by random jmps below  
jz get_next_mod ; When we reach the start of the EAT (we search backwards), process the next module  
dec ecx ; Decrement the function name counter  
mov esi, [ebx+ecx\*4] ; Get rva of next module name  
add esi, edx ; Add the modules base address  
xor edi, edi ; Clear EDI which will store the hash of the function name  

test ecx, ecx

Realiza uma verificação entre ECX e ECX

jz get_next_mod

Jump near if 0, salta para a função get_next_mod caso o resultado da ultima operação matematica seja zero, ou seja, caso ECX (que é nosso contador de funções) tenha chegado a zero, salta para o ponto de código responsável por iniciar o processo de verificação do próximo módulo. Caso seja ECX maior que zero, continua a execução para a proxima instrução.

ECX diferente de zero, então o JMP não irá ocorrer

dec ecx

Decrementa 01 de ECX

mov esi, [ebx + ecx * 4]

Resgata o RVA do nome da função. Onde:

  • EBX: Contém o VMA do início da array que detém o nome das funções
  • ECX: índice numérico dentro da função
  • ECX * 4: índico numérico multiplicado por 4 Bytes (32 bits) que representa cada endereço que contém o nome da função

add esi, edx

Adiciona o RVA com o BaseAddress do módulo atual para obter o VMA da do nome da função e o salva no registrador EAX

Como no decorrer dest loop iremos decrementando o ECX, na pratica vamos varrendo a lista de traz p/ frente, sendo assim na primeira intereção tremos o nome da última função do array.

xor edi, edi

Zera o registrador EDI para utiliza-lo como armazenamento do hash da função

Função loop_funcname

loop_funcname: ;  
xor eax, eax ; Clear EAX  
lodsb ; Read in the next byte of the ASCII function name  
ror edi, 0xd ; Rotate right our hash value  
add edi, eax ; Add the next byte of the name  
cmp al, ah ; Compare AL (the next byte from the name) to AH (null)  
jne loop_funcname ; If we have not reached the null terminator, continue  
add edi, [ebp-8] ; Add the current module hash to the function hash  
cmp edi, [ebp+0x24] ; Compare the hash to the one we are searchnig for  
jnz get_next_func ; Go compute the next function hash if we have not found it  
; If found, fix up stack, call the function and then value else compute the next one...  
pop eax ; Restore the current modules EAT  
mov ebx, [eax+0x24] ; Get the ordinal table rva  
add ebx, edx ; Add the modules base address  
mov cx, [ebx+2\*ecx] ; Get the desired functions ordinal  
mov ebx, [eax+0x1c] ; Get the function addresses table rva  
add ebx, edx ; Add the modules base address  
mov eax, [ebx+4\*ecx] ; Get the desired functions RVA  
add eax, edx ; Add the modules base address to get the functions actual VA  

xor eax, eax

Zera o registrador EAX

lodsb

Carrega o primeiro byte vindo do ESI para o registrador AL.

ror edi, 0xd

Rotaciona 0xd (decimal, 13) bits para a direita do valor presente no EDI (Hash Value)

add edi, eax

Adiciona o byte (resgatado do nome da função) ao valor presente no EDI e salva o resultado no próprio EDI

cmp al, ah

Compara o byte copiado pela função lodsb salvo em AL com o registrador AH (que neste cenário será zero)

jne loop_funcname

Jump near if not equal, verifica se o resultado da última comparação não é iguial, ou seja, se o último byte copiado em AL é diferente de zero, caso seja diferente de zero retorna para o início da função loop_funcname para continuar copiando os bytes do nome da função e assim calculando o hash. Caso tenha chegado no terminador de string \0 (NULL Byte) continua para a próxima instrução

add edi, [ebp-8]

Soma o hash do nome da função recem cálculada com o hash do nome do módulo calculado anteriormente e salvo em ebp-8, salvando o resultado no registrador EDI

cmp edi, [ebp+0x24]

Compara se o hash cálculado é igual ao hash desejado. Onde:

  • EDI: Hash cálculado com o nome do módulo + Nome da função
  • EBP + 0x24: Posição da memória que detém o Hash da função desejada. Em nosso exemplo, este Hash foi adicionado na pilha com o PUSH 0x56A2B5F0 que é o hash da função ExitProcess

Vamos então colocar um breakpoint nessa função para verificar após o cálculo do hash do nome de cada função + o hash do módulo, tendo então o hash final da função para posteriormente poder verificar se é igual ao desejado.

jnz get_next_func

Jump near if not zero, caso a comparação anterior aponte como hash diferentes o código será direcionado para a função get_next_func, responsável por verificar a próxima função exportada do módulo atual. Caso os hashes sejam iguais continua para o fluxo da proxima instrução.

pop eax

Restaura para o registrador EAX o VMA da tabela de exports do módulo atual. Este valor foi salvo na pilha através do PUSH EAX realizado anteriormente.

Colocamos um breakpoint nessa instrução, pois só chegaremos nela no momento em que os hashes forem iguais e posteriormente liberei a execução.

mov ebx, [eax+0x24]

Relembrando a estrutura da Export table

Temos no offset 0x24 o array AddressOfNameOrdinals, sendo assim esta instrução copia o VMA do array AddressOfNameOrdinals para o registrador EBX

add ebx, edx

Adiciona o RVA com o BaseAddress do módulo atual para obter o VMA da do array AddressOfNameOrdinals e o salva no registrador EBX

mov cx, [ebx + 2 * ecx]

Em ECX temos o índice da função desejada dentro da array AddressOfNames, como os arrays AddressOfNames e AddressOfNameOrdinals utilizam o mesmo índice podemos reaproveita-lo para endontrar o RVA da função dentro do array AddressOfNameOrdinals. Dentro da array AddressOfNames utiliamos ECX * 4 para saltar em cada um dos registros da array, pois cada registro dentro da AddressOfNames é um valor DWORD, ja na array AddressOfNameOrdinals cada registro é um WORD, sendo assim iremos multiplicar por 0x02 para saltar em cada registro. Conforme podemos observar na tabela de exports do módulo kernel32.dll

mov ebx, [eax + 0x1c]

Antes de utilizar o novo índice calculado anteriormente iremos pegar o RVA do AddressOfFunctions no índice 0x1c da Export table e o salva no registrador EBX

add ebx, edx

Adiciona o RVA com o BaseAddress do módulo atual para obter o VMA da do array AddressOfFunctions e o salva no registrador EBX

mov eax, [ebx + 4 * ecx]

Resgata o RVA da função desejada dentro da array AddressOfFunctions utilizando o offset resgatado da array AddressOfNameOrdinals. Onde:

  • EDX: Endereço virtual do AddressOfFunctions
  • ECX: Índice da função desejada (resgatado do array AddressOfNameOrdinals)
  • ECX * 4: Índice da função * 4 bytes de cada endereço

add eax, edx

Adiciona o RVA com o BaseAddress do módulo atual para obter o VMA da função desejada e o salva no registrador EAX

Este ja é o endereço de execução da função e pode ser usado pela instrução call eax (por exemplo).

Funçao finish

finish:  
mov [esp+0x24], eax ; Overwrite the old EAX value with the desired api address for the upcoming popad  
pop ebx ; Clear off the current modules hash  
pop ebx ; Clear off the current position in the module list  
popad ; Restore all of the callers registers, bar EAX, ECX and EDX which are clobbered  
pop ecx ; Pop off the origional return address our caller will have pushed  
pop edx ; Pop off the hash value our caller will have pushed  
push ecx ; Push back the correct return value  
jmp eax ; Jump into the required function  

mov [esp+0x24], eax

Altera o valor orginal do EAX adicionado na pilha pelo pushad para o endereço da função desejada (que recem calculamos). Este processo é necessário pois daqui algumas instruções iremos restaurar os registrados como estavm no momento da chamada da nossa função. Neste momento o WAX conterá o VMA da função que desejamos chamar.

pop ebx

Remove da pilha o hash do módulo atual

pop ebx

Remove da pilha a posição atual na listagem de módulos

popad

Restaura todos os registradores conforme seus valores iniciais. Nota para o EAX que será restaurado com o valor que sobrescrevemos a 2 instruçÕes.

pop ecx

Remove da pilha e copia para o registrador ECX o endereço de retorno do nosso fluxo de execução original. Este endereço foi adicionado automaticamente na pilha no momento da chamada da instrução CALL.

pop edx

Remove da pilha o hash da função que adicionamos antes da chamada da função call

push ecx

Adiciona novamente na pilha o endereço de retorno para que após a execução da função desejada o código possa continuar sua execução normalmente.

jmp eax

Salta para o endereço da função em que se deseja executar. Do ponto de vista do fluxo de código não continuaremos para as próximas instruções que será estudadas a segir, pois uma vez saltado para a função desejada a mesma finalizará com um RET que retornará, então, para nosso fluxo de execução original.

Funções adicionais

get_next_mod: ;  
pop eax ; Pop off the current (now the previous) modules EAT  
get_next_mod1: ;  
pop edi ; Pop off the current (now the previous) modules hash  
pop edx ; Restore our position in the module list  
mov edx, [edx] ; Get the next module  
jmp next_mod ; Process this module  

Conforme podemos visualizar ha outras funções no final do código da biblioteca que ja foram referenciados anteriormente e fazem parte do processo de execução.

Conclusão

Como vimos através do estudo da biblioteca block_api é possível localizar em tempo de execução todos os módulos carregados no sistema, inclusive os carregados em tempo de execução, e suas respectivas funções exportadas.

Treinamento

Deseja aprender passo a passo como realizar a criação de um Shellcode? Então da uma olhada em nosso treinamento de Shellcoding onde vamos do zero a criação de um shell reverso windows e linux, passando por shellcoding 32 e 64 bits.

Link do treinamento: https://sec4us.com.br/treinamentos/shellcoding-para-desenvolvimento-de-exploits/

Fontes

OSWE - Uma história de insucessos!

Shellcoding - Finding the address of the function dynamically. Analysis of the block_api library.

Comments powered by Disqus.