Native DLL Injection Using LoadLibrary - Parte 04

Por Davi Chaves

O processo de injeção nativa de uma dll em um processo é bastante semelhante ao método convencional utilizando as funções da kernel32.dll. A lógica é exatamente igual:

NTSTATUS NtProcessStartup(PPEB peb) {
	// Abre o processo alvo
	NtOpenProcess(...); 
	// Alocamos um pouco de memória no processo alvo
	NtAllocateVirtualMemory(...);
	// Escrevemos o nome da nossa dll na memória do processo alvo
	NtWriteVirtualMemory(...);
	// Pegamos o endereço da função LoadLibraryW
	LdrGetProcedureAddress("LoadLibraryW"...);
	// Criamos uma thread no exato endereço da função LoadLibraryW no processo alvo
	RtlCreateUserThread(...);
}

NtOpenProcess

O primeiro passo é conseguirmos um handle do processo alvo com permições de leitura/escrita em relação a memória, permissão para realizar operações nesse processo (ex.: WriteProcessMemory, VirtualProtectEx) e permissão para criamos uma thread nesse processo.

HANDLE hTgtProc;
CLIENT_ID cid{ ULongToHandle(1337) };
OBJECT_ATTRIBUTES procAttr = RTL_CONSTANT_OBJECT_ATTRIBUTES(nullptr, 0);
NTSTATUS status = NtOpenProcess(&hTgtProc, PROCESS_VM_WRITE | PROCESS_VM_OPERATION | PROCESS_CREATE_THREAD, &procAttr, &cid);

if (!NT_SUCCESS(status)) // error

A função recebe como parâmetro o handle que será populado, o acessmask desejado, os atributos do processo (o mais básico possível, no nosso caso) e o clientid, que serve para identificar o nosso processo. Além disso, usarei um pid hardcoded por simplicidade, mas o ideal seria passar isso como argumento por command line, por exemplo.

NtAllocateVirtualMemory

O proximo passo é alocarmos memória nesse processo alvo. É nesse espaço de memória que vamos escrever o caminho da nossa dll.

PVOID pBuffer = nullptr;
SIZE_T size = 4096;
status = NtAllocateVirtualMemory(hTgtProc, &pBuffer, 0, &size, MEM_COMMIT | MEM_RESERVE, PAGE_READWRITE);

Observe que a função vai nos dar o endereço de um buffer (pBuffer) contendo o endereço da memória alocado no processo alvo. É importante entender que esse endereço não está em nossa memória. Além disso, temos que passar o tamanho que queremos alocar. Como só é possível alocar páginas de memória (tamanho de 4096 bytes), qualquer valor maior que zero serviria, já que queremos apenas alocar o caminho de uma dll. ![[Screen-Recording-12-10-2023-11-47-29-AM.gif]] Veja que no exato momento no qual chamamos a função NtAllocateVirtualMemory um segmento de memória de 0x1000 bytes é alocado no endereço 0x2713A9E0000.

NtWriteVirtualMemory

Agora só precisamos escrever o caminho da dll que desejamos injetar na memória do processo alvo que acabamos de alocar.

WCHAR dllpath[] = L"C:\\Cool\\dll\\path.dll";
status = NtWriteVirtualMemory(hTgtProc, pBuffer, dllpath, sizeof(WCHAR) * (wcslen(dllpath) + 1), nullptr);

![[Screen-Recording-12-10-2023-12-03-35-PM.gif]] Veja o exato momento no qual escrevemos o caminho da dll no endereço que acabamos de alocar na memória do processo alvo.

LdrGetDllHandle & LdrGetProcedureAddress

Agora que temos o caminho da nossa dll que queremos executar no espaço de memória do processo alvo, só precisaríamos chamar de algum jeito a função LoadLibraryW no processo alvo (passando o endereço da string como parâmetro). Mas como vamos fazer isso? É importante notar que, como o nosso processo alvo (notepad, no meu caso) importa o kernel32.dll, a função LoadLibraryW está definida em algum lugar, só precisamos achar aonde. Mas como podemos fazer isso? Simples! Todas essas dlls do sistema são mapeadas no mesmo espaço de memória entre todos os processos. Portanto, se descobrirmos aonde está a função LoadLibraryW em um processo, vamos saber aonde ela está em todos os outros processos que importam essa biblioteca dinâmicamente. ![[Pasted image 20231210122512.png]] Na imagem acima, temos três processos diferentes (notepad, totalpe e x64dbg) que importam o kernel32.dll. Perceba que ela mapeada no mesmo endereço de memória em todos os processos! Isso vale para as outras dlls, como a própria ntdll.dll. Agora precisamos do endereço da função LoadLibraryW. Se estivessemos em uma aplicação normal, utilizando a kernel32.dll, poderiamos alcançar isso apenas fazendo algo como pvoid address = LoadLibraryW! Entretando, como não temos essa dll na memória do nosso processo, teriamos que, de algum outro jeito, pegar o endereço dessa função. O código, caso fossemos utilizar essa estratégia, seria esse:

UNICODE_STRING kernel32Name;
RtlInitUnicodeString(&kernel32Name, L"kernel32");
PVOID hK32Dll;
status = LdrGetDllHandle(nullptr, nullptr, &kernel32Name, &hK32Dll);

ANSI_STRING fname;
RtlInitAnsiString(&fname, "LoadLibraryW");
PVOID pLoadLibrary;
status = LdrGetProcedureAddress(hK32Dll, &fname, 0, &pLoadLibrary);

// Depois é só chamar RtlCreateUserThread passando a pLoadLibrary e nosso buffer

Enquanto eu estava escrevendo esse artigo, eu havia esquecido desse detalhe. Diante disso, eu procurei alguma solução para conseguirmos esse endereço, mas não achei nenhum jeito simples e elegante. Poderiamos tentar chamar a função LdrLoadDll da ntdll.dll, entretando ela recebe quatro parametros ao invés de apenas um. De qualquer forma, essa estratégia é válida caso você estivesse escrevendo uma aplicação que importasse a kernel32.dll. De qualquer forma, vamos para o plano B que será explicado na parte 05.