Seções iMasters
Desenvolvimento

Advanced DLL Injection

Neste artigo, irei abordar um assunto trivial (como pode parecer) como a injeção de DLL. Por alguma razão, a maioria dos tutoriais na web nos dá apenas uma breve pincelada do tema, principalmente se limitando à chamada da função LoadLibraryA/W da API do Windows no espaço de endereço de outro processo. Embora não seja de todo ruim, isso nos fornece a solução menos flexível. O que significa que toda a lógica deve ser codificada no DLL que queremos injetar. Por outro lado, podemos incorporar toda a gestão de configuração (carregar os arquivos de configuração, a análise do mesmo etc.) em nossa DLL. Isso é melhor, mas ainda a enche com código que só vai ser executado uma vez.

Vamos tentar outra abordagem. O que faremos é escrever um carregador (um executável que vai injetar nossa DLL em outro processo) e uma DLL pequeno, que será injetada. Para simplificar, o carregador também criará o processo que será o alvo. Sendo um usuário do Linux, eu usei Flat Assembler e mingw32 para essa tarefa, mas você pode ajustar o código para qualquer ambiente que preferir.

Uma breve observação para os nerds antes de começarmos. O código neste artigo não contém quaisquer verificações de segurança (por exemplo, correção de verificação do valor retornado pela função específica), a menos que seja necessário como um exemplo. Caso decida experimentá-lo, você estará fazendo isso por sua própria conta e risco.

Então, vamos começar a diversão.

Criação do processo que será o alvo

Vamos supor que o carregador já passou a fase de carregamento e análise de arquivos de configuração e está pronto para iniciar o trabalho real.

O Windows nos fornece todas as ferramentas que precisamos para iniciar um processo. Há mais de uma maneira de fazer isso, mas vamos usar a mais simples: a função de API do CreateProcess. Sua declaração parece muito assustadora, mas vamos fazê-la da maneira mais fácil possível:

   BOOL WINAPI CreateProcess(
__in_opt LPCTSTR lpApplicationName,
__inout_opt LPTSTR lpCommandLine,
__in_opt LPSECURITY_ATTRIBUTES lpProcessAttributes,
__in_opt LPSECURITY_ATTRIBUTES lpThreadAttributes,
__in BOOL bInheritHandles,
__in DWORD dwCreationFlags,
__in_opt LPVOID lpEnvironment,
__in_opt LPCTSTR lpCurrentDirectory,
__in LPSTARTUPINFO lpStartupInfo,
__out LPPROCESS_INFORMATION lpProcessInformation
);

Ao chamar essa função, nós só temos que especificar metade dos parâmetros e definir todo o resto para NULL. Essa função possui duas variantes CreateProcessA e CreateProcessW como versões ASCII e Unicode, respectivamente. Vamos continuar fiéis ao ASCII o tempo todo, então, o nosso código ficaria assim (devido ao fato de que “CreateProcess” é mais um macro do que nome de função, devemos especificar a versão A, pois alguns compiladores tendem a usar como padrão a versão W):

CreateProcessA(nameOfTheFile, NULL, NULL, NULL, FALSE, CREATE_SUSPENDED, NULL, NULL, &startupInfo, &processInformation);

Não se esqueça de definir o campo cb do startupInfo para (DWORD) sizeof (STARTUPINFO), caso contrário ele não irá funcionar.

Se a função tiver êxito, obteremos todas as informações sobre o processo (handles e IDs) na estrutura processInformation, que tem o seguinte protótipo:

 typedef struct _PROCESS_INFORMATION
{
HANDLE hProcess; //Handle to the process
HANDLE hThread; //Handle to the main thread of the process
DWORD dwProcessId; //ID of the new process
DWORD dwThreadId; //ID of the main thread of the process
}PROCESS_INFORMATION, *LPPROCESS_INFORMATION;

A essa altura, o processo foi criado, mas está suspenso. Isso significa que ele ainda não iniciou a sua execução e não vai até chamarmos ResumeThread (processInformation.dwThreadId) dizendo ao sistema operacional para retomar a thread principal do processo, mas essa vai ser a última ação realizada pelo nosso carregador.

Lancet

Pode-se chamá-lo de shellcode, mas não tem nada a ver com a carga viral ou qualquer outra intenção maliciosa (a menos que alguém diga que invadir o espaço de endereços de outro processo é malicioso). É o código que injetaremos no processo que será o alvo. Ele, teoricamente, pode ser escrito em qualquer linguagem, desde que seja independente da posição e compilado em instruções nativas (em nosso caso, instruções x86), mas eu prefiro fazer essas coisas em linguagem Assembly.

É sempre uma boa ideia pensar no que o seu código se destina a fazer antes de escrever uma única linha dele. Nesse caso, é uma ideia de ouro. O código precisa ser pequeno, de preferência rápido e estável, pois pode dar um pouco de dor de cabeça para depurar, uma vez que tiver sido injetado.

Existem duas tarefas básicas que você vai querer atribuir a esse código:

  • Carregar a nossa DLL
  • Chamar o procedimento de inicialização exportado pela nossa DLL

e uma condição inevitável – ele tem que ser uma função declarada como callback ThreadProc, devido ao fato de que usaremos a função CreateRemoteThread para lançá-lo. O protótipo de uma função callback ThreadProc se parece com isto:

DWORD WINAPI ThreadProc( __in LPVOID lpParameter);

o que significa que ele tem que retornar um valor do tipo DWORD. Ele aceita um parâmetro, que pode ser um valor real (mas você precisa convertê-lo para tipo LPVOID) ou um ponteiro para um array de parâmetros. Só mais uma coisa sobre essa função (a última mas não menos importante!): é uma função stdcall macro WINAPI definida como __declspec (stdcall). Isso significa que nossa função precisa cuidar da limpeza da pilha antes do retorno. No nosso caso, é muito fácil, basta usar ret 0×04 (assumindo que o tamanho de LPVOID seja de 4 bytes).

Outra coisa importante a mencionar – você vai, obviamente, precisar saber quantos bytes sua função ocupa, a fim de alocar memória no espaço de endereço do processo que será o alvo e mover seu código corretamente lá. Além da alocação de um bloco de memória executável para nossa função, você precisa alocar um bloco de dados – definições de configuração a serem passadas para a DLL injetada. É fácil passar o endereço dos parâmetros como um argumento para a nossa ThreadProc.

O esqueleto da função ficaria assim:

 lancet:
push ebp
mov ebp, esp
sub esp, as_much_space_as_you_need_for_variables
push registers_you_are_planning_to_use

;function body

pop registers_you_used
mov esp, ebp
pop ebp
ret 0x04
lancet_size = $-lancet

A última linha nos dá o tamanho exato da função em bytes. A seguir está o template de código fonte:

format MS COFF ;as we are going to link this file with our loader
public lancet as '_lancet'
section '.text' readable executable
lancet:
;our function goes here
;followed by data
loadLibraryA db 'LoadLibraryA',0
init db 'name_of_the_initialization_function',0
ourDll db 'name_of_our_dll',0
kernel32 db 'kernel32.dll',0
lancet_size = $-lancet
public lsize as '_lancet_size'
section '.data' readable writeable
lsize dd lancet_size

Então, o que é que vamos inserir no “corpo da função”? Primeiramente, como o nosso código, uma vez injetado, não tem ideia de onde ele está na memória, devemos salvar o nosso “endereço base” e calcular todas os offsets relativos a esse endereço. Isso é feito de uma maneira simples. Chamamos o próximo endereço e colocamos o endereço de retorno em nossa variável local.

   call @f
@@:
pop dword [ebp-4]
sub dword [ebp-4], @b-lancet

É isso. Agora a variável em [ebp-4] contém o nosso “endereço base”. Cada vez quisermos chamar outra função ou acessar nossos dados (strings com nomes, lembra?) nós devemos fazer o seguinte:

   mov  ebx, [ebp-4]
add ebx, ourDll-lancet
push ebx
mov ebx, [ebp-8] ;assume that we stored the address of LoadLibraryA at [ebp-8]
call dword ebx

O código acima é um equivalente a LoadLibraryA (“name_of_our_dll”).

Agora vamos falar sobre a execução propriamente dita. Embora saibamos onde estamos, não temos idsia de qual é o endereço de LoadLibraryA. Há, pelo menos, duas maneiras de obter o endereço direitinho. A primeira foi descrita neste meu artigo. A segunda também é interessante – PEB. Sim, nós estamos indo acessar o Process Environment Block, encontrar a estrutura LDR_MODULE que se refere ao KERNEL32.DLL, e obter o seu endereço de base (que é também um handle para a biblioteca). Alguns podem dizer que essa forma não é confiável, não é estável e que é até perigosa, mas eu digo que declarações como essas não são sérias. Não vamos mudar nada nessas estruturas. Nós só vamos analisá-las.

De que jeito podemos encontrar o PEB? Isso é muito simples. Ele está localizado em [FS:0x30]. Uma vez que o temos, estamos a caminho do endereço PEB_LDR_DATA, que está no PEB+0x0C. A fim de analisar a estrutura PEB_LDR_DATA, devemos declarar o seguinte em nosso código Assembly:

struc list_entry
{
.flink dd ? ;pointer to next list_entry structure
.blink dd ? ;pointer to previous list_entry structure
}


struc peb_ldr_data
{
.length dd ?
.initialized db ?
db ?
db ?
db ?
.ssHandle dd ?
.inLoadOrderModuleList list_entry ;we are going to use this list
.inMemoryOrderModuleList list_entry
.inInitializationOrderModuleList list_entry
}


struc ldr_module
{
.inLoadOrderModuleList list_entry ;pointers to previous and next modules in list
.inMemoryOrderModuleList list_entry
.inInitializationOrderModuleList list_entry
.baseAddress dd ? ;This is what we need!
.entryPoint dd ?
.sizeOfImage dd ?
.fullDllName unicode_string ;full path to the module file
.baseDllName unicode_string ;name of the module file
.flags dd ?
.loadCount dw ?
.tlsIndex dw ?
.hashTable list_entry
.timeDateStamp dd ?
}

Vou deixar a implementação da função de análise de módulo de lista pra você. Você só tem que manter em mente que a string que você vai verificar é representada pela estrutura UNICODE_STRING (descrita no artigo mencionado acima). Outra coisa para lembrar é que é melhor implementar a função case insensitive de comparação de strings.

Depois de encontrar o LDR_MODULE, cuja baseDllName é “kernel32.dll”, você tem seu handle (apenas no campo baseAddress). Você pode usar a função _get_proc_address do mesmo artigo (mencionado acima) para obter o endereço da função LoadLibraryA. Tendo esse endereço, você está pronto para carregar sua DLL (faça a injeção real). Sugestão pessoal – não colocar um monte de código para a função DllMain.

LoadLibraryA retorna um handle para a DLL recém-carregada, que você pode usar para localizar a função de inicialização (lembre que ela tem que ser exportada por sua DLL e de preferência usar a convenção stdcall). Depois que você usar _get_proc_address em sua função de inicialização, chame-a e passe o endereço do bloco de dados como um parâmetro (que foi passado para a nossa função lancet como um parâmetro na pilha):

   push dword [ebp+8]  ;parameter passed to lancet is here
call dword [ebp-12] ;assume that you stored the address of the initialization
;function here

É isso aí. O seu código agora pode retornar. A DLL foi injetada e inicializada.

Injeção

De alguma forma, perdemos o empolgante processo de injeção de nosso código lancet. Não se preocupe, não me esqueci disso.

Como já mencionei acima, temos que alocar dois blocos – para código e para dados. Isso pode ser feito chamando a função VirtualAllocEx, o que permite alocações de memória no espaço de endereço de outro processo.

LPVOID WINAPI VirtualAllocEx(
__in HANDLE hProcess,
__in_opt LPVOID lpAddress,
__in SIZE_T dwSize,
__in DWORD flAllocationType,
__in DWORD flProtect
);

Use MEM_COMMIT como flAllocationType, e PAGE_EXECUTE_READWRITE e PAGE_READWRITE para alocação de código e bloco de dados, respectivamente. Essa função retorna o endereço do bloco alocado no espaço de endereço do processo especificado ou NULL.

A função WriteProcessMemory da API é usada para copiar o código e os dados no espaço de endereço do processo que será o alvo.

BOOL WINAPI WriteProcessMemory(
__in HANDLE hProcess,
__in LPVOID lpBaseAddress,
__in LPCVOID lpBuffer,
__in SIZE_T nSize,
__out SIZE_T*lpNumberOfBytesWritten
);

Depois de ter copiado tanto o dado quanto o código, você vai querer chamar sua função thread. A única maneira de chamar uma função que reside na memória de outro processo é chamando a API CreateRemoteThread.

HANDLE WINAPI CreateRemoteThread(
__in HANDLE hProcess, //the handle to our process
__in LPSECURITY_ATTRIBUTES lpThreadAttributes, //may be NULL
__in SIZE_T dwStackSize, //may be 0
__in LPTHREAD_START_ROUTINE, //the address of our code block
__in LPVOID lpParameter, //the address of our data block
__in DWORD dwCreationFlags, //may be 0
__out LPDWORD lpThreadId //may be NULL
);

Essa função retorna um handle para a thread remota, que, por sua vez, pode ser passada para a função  WaiForSingleObject da API, para que possamos receber uma notificação em seu retorno.

Eu decidi não abordar as possibilidades que sua DLL pode fazer enquanto estiver anexada ao processo que será o alvo. Deixo isso pra você.

Espero que este artigo não tenha sido muito confuso, e sim útil.

?

Texto original disponível em http://syprog.blogspot.com.br/2011/11/advanced-dll-injection.html

Mensagem do anunciante:

Torne-se um Parceiro de Software Intel®. Filie-se ao Intel® Developer Zone. Intel®Developer Zone

Qual a sua opinião?