Desenvolvimento

10 out, 2012

CreateRemoteThread. Bypass Windows 7 Session Separation

Publicidade

A Internet está cheia de fóruns de programadores, e esses fóruns estão cheios de perguntas sobre a função CreateRemoteThread da API do Windows não funcionando no Windows 7 (ao tentar injetar uma DLL). Essas mensagens, de alguma forma, te redirecionam para a página do MSDN dedicada a essa API, que diz: “Terminal Services isola cada sessão de terminal pelo design. Portanto, CreateRemoteThread falhará se o processo de destino estiver em uma sessão diferente da do processo de chamada”, o que basicamente significa iniciar o processo a partir do seu injetor como suspenso, injetar sua DLL e, em seguida, reiniciar a thread principal do processo. Isso funciona… Na maioria das vezes… Mas às vezes você realmente precisa injetar seu código em um processo em execução. Não existe uma maneira de fazer isso? Bem, existe. Na verdade, é tão fácil, que eu decidi não anexar meu código fonte a este artigo (principalmente, porque eu sou muito preguiçoso para fazer com que ele pareça legível), então eu fiz upload do código fonte.

Vou começar como de costume, com uma nota para nerds, a fim de evitar comentários sem sentido e discussões estúpidas.

O código fornecido dentro do artigo é apenas para fins de exemplo. As verificações de erro foram omitidas de propósito. Sim, pode haver outra maneira, provavelmente melhor ainda, de fazer isso. Não, mapeamento DLL manual não é melhor, a menos que você tenha tempo de sobra e não tenha nada a ver com isso.

Para todos os outros, vamos aos negócios!

Abertura do processo vítima

Esta é a parte mais fácil. Nesta fase, você vai ver se é capaz de injetar seu código ou não (no caso de um processo de sistema, por exemplo). Nada incomum aqui – você simplesmente chama a boa e velha API OpenProcess.

HANDLE WINAPI OpenProcess(
DWORD dwDesiredAccess, /* in our case PROCESS_ALL_ACCESS */
BOOL  bInheritHandle, /* no need, so FALSE */
DWORD dwProcessId /* self explanatory enough */
);

que abre o processo especificado por dwProcessId e retorna um handle para esse processo, a menos que você não possua direitos de acesso suficientes para ele.

Lendo o Shellcode

O que você costuma ver nos exemplos de shellcode através da internet é um array de caracteres não definidos de valores hexadecimais em algum lugar do código C. Ele ajuda a manter a quantidade de arquivos menor, mas não é muito legal de se lidar. Eu decidi armazenar o shellcode em um arquivo binário separado, produzido com FASM (Flat Assembler):

use32
; offset of the LoadLibraryA address within the shellcode
dd    func
; save all registers
push  eax ebx ecx edx ebp edi esi
; get your EIP
call  next
next:
pop   eax
mov   ebx, eax
; get the address of the DLL name
mov   eax, string - next
; do this to avoid possible negative values (due to sign extend)
movzx eax, al
add   eax, ebx
; pass it to the LoadLibraryA API
push  eax
; get the address of the LoadLibraryA function
mov   eax, func - next
movzx eax, al
add   eax, ebx
mov   eax, [eax]
; call LoadLibraryA
call  eax
; restore registers
pop   esi edi ebp edx ecx ebx eax
; return
ret
func     dd 0x12345678 ; placeholder for the address
string:

A compilação desse código com FASM.EXE vai produzir um raw binário, no qual todos os offsets são baseados em 0. Existem algumas partes do código acima que podem exigir alguma explicação adicional (por exemplo, por que não termina com ExitThread()). Estou ciente disso e vou explicar um pouco mais tarde.

Por hora, aloque um buffer unsigned char para o seu shellcode. Faça com que esse buffer seja grande o suficiente para conter o shellcode e o nome da DLL (minha suposição é que você passou esse nome como um parâmetro de linha de comando para o seu injetor) com a sua terminação sendo zero.

Uma vez lido o shellcode naquele buffer, acrescente o nome da DLL (que pode ser um caminho completo até ela) para o fim do shellcode com, por exemplo, a função memcpy(). Já chegamos na metade do caminho! Agora ainda temos que “contar” ao shellcode onde a função LoadLibraryA da API está localizada na memória. Felizmente, a randomização do endereço de carregamento no Windows está longe de ser perfeita (os endereços dos módulos carregados podem variar entre reinicializações subsequentes, mas são os mesmos para todos os processos). Isso significa que, assim como na injeção de DLL usual, nós obtemos o endereço dessa API em nosso processo chamando o bom e velho GetProcAddress (GetModuleHandleA (“kernel32.dll”), “LoadLibraryA”) e salvando-o para a variável “func” do shellcode. Devido ao fato de que nosso shellcode poder variar em tamanho ao longo do tempo (isso depende das necessidades), salvamos o offset para essa variável nos primeiros quatro bytes do shellcode, o que elimina a necessidade de codificá-lo. Basta fazer o seguinte:

*(unsigned int*)(shellcode_ptr + *(int*)(shellcode_ptr)) = (unsigned int)LoadLibraryA_address;

O nosso shellcode está pronto agora.

“Criar tarefa remota” sem CreateRemoteThread()

Como o título deste parágrafo sugere – não vamos usar o CreateRemoteThread(). Na verdade, nós não vamos criar qualquer thread no processo vítima (bem, a DLL injetada pode, mas o shellcode não).

Injeção de código

Certamente, precisamos mudar o nosso shellcode para o endereço do espaço do processo vítima para carregamento ou biblioteca. Estamos fazendo isso da mesma maneira, como copiaríamos o nome da DLL no procedimento regular de injeção de DLL.

1. Alocar a memória no processo remoto com

LPVOID WINAPI VirtualAllocEx(
HANDLE hProcess, /* the handle we obtained with OpenProcess */
LPVOID lpAddress, /* preferred address; may be NULL */
SIZE_T dwSize, /* size of the allocation in bytes */
DWORD  flAllocationType, /* MEM_COMMIT */
DWORD  flProtect /* PAGE_EXECUTE_READWRITE */
);

Essa função retorna o endereço da alocação no espaço de endereço (?) do processo vítima ou NULL se ele falhar.

2. Copie o shellcode (?) para o buffer (?) que acabamos de alocar no espaço de endereço (?) do processo vítima:

BOOL WINAPI WriteProcessMemory(
HANDLE   hProcess, /* same handle as above */
LPVOID   lpBaseAddress, /* address of the allocation */
LPCVOID  lpBuffer, /* address of the local buffer with the shellcode */
SIZE_T   nSize, /* size of the shellcode together with the appended
NULL-terminated string */

3.

SIZE_T   *lpNumberOfBytesWritten /* if this is zero - check your code */
);

Se o valor de retorno dessa função for não-zero, nós copiamos com êxito o nosso shellcode para o endereço do espaço do processo vítima. Também pode ser uma boa ideia verificar o valor retornado em lpNumberOfBytesWritten.

Faça-o funcionar

Então, a gente copiou o nosso shellcode. A única coisa que resta é fazê-lo funcionar, mas não podemos usar a API CreateRemoteThread()… A solução é um pouco mais complicada.

Antes de tudo, temos que suspender todos as threads do processo vítima. Em geral, suspender apenas uma é o suficiente, mas, como não sabemos ao certo o que está acontecendo lá, devemos suspender todas elas. Não há nenhuma API específica que nos forneça a lista de threads para um processo específico; em vez disso, temos de criar um snapshot com CreateToolhelp32Snapshot, que nos fornece a lista de todos as threads em execução de todos os processos em execução no sistema:

HANDLE WINAPI CreateToolhelp32Snapshot(
DWORD dwFlags, /* TH32CS_SNAPTHREAD = 0x00000004 */
DWORD th32ProcessID /* in this case may be 0 */
);

Essa função retorna o handle para o snapshot, que contém informações sobre todos as threads presentes. Uma vez que temos isso, “iteramos através da lista” com Thread32First e funções da API Thread32Next:

BOOL WINAPI Thread32First(
HANDLE hSnapshot, /* the handle to the snapshot */
LPTHREADENTRY32 lpte /* pointer to the THREADENTRY32 structure */
);

O Thread32Next tem o mesmo protótipo do Thread32First.

typedef struct tagTHREADENTRY32{
DWORD dwSize; /* size of this struct; you have to initialize this field before use */
DWORD cntUsage;
DWORD th32ThreadID; /* use this value to open thread for suspension */
DWORD th32OwnerProcessID; /* compare this value against the PID of the victim
to filter out threads of other processes */
LONG  tpBasePri;
LONG  tpDeltaPri;
DWORD dwFlags;
} THREADENTRY32, *PTHREADENTRY32;

Para cada THREADENTRY32 com o th32OwnerProcessID correspondente, abra-a com OpenThread() e suspenda com SuspendThread:

HANDLE WINAPI OpenThread(
DWORD dwDesiredAccess, /* THREAD_ALL_ACCESS */
BOOL  bInheritHandle, /* FALSE */
DWORD dwThreadId /* th32ThreadID field of THREADENTRY32 structure */
);

e

DWORD WINAPI SuspendThread(
HANDLE hThread, /* Obtained by OpenThread() */
);

Não se esqueça de CloseHandle(openedThread) 🙂

Pegue a primeira thread, uma vez que ela esteja aberta (na verdade, você pode fazer isso com qualquer thread que pertença ao processo vítima) e suspensa, e obtenha o seu CONTEXT (ver “Community Additions” aqui) usando a API GetThreadContext:

BOOL WINAPI GetThreadContext(
HANDLE    hThread, /* handle to the thread */
LPCONTEXT lpContext /* pointer to the CONTEXT structure */
);

Agora, quando todos as threads do processo vítima estão suspensos, podemos fazer o nosso trabalho. A ideia é redirecionar o fluxo de execução dessa thread para o nosso shellcode (?), mas fazê-lo de tal forma, que o shellcode (?) voltaria para onde a thread suspensa está atualmente. Esse não é um problema de todos, pois temos o CONTEXT da thread. O código a seguir faz isso muito bem:

/* "push" current EIP of the thread onto its stack, so that the ret instruction in the shellcode returns the execution flow to this address (which is somewhere in WaitForSingleObject for suspended threads) */
ctx.Esp -= sizeof(unsigned int);
WriteProcessMemory(victimProcessHandle,
(LPVOID)ctx.Esp,
(LPCVOID)&ctx.Eip,
sizeof(unsigned int),
&bytesWritten);
/* Set the EIP to our injected shellcode; do not forget to skip the first four bytes */
ctx.Eip = remoteAddress + sizeof(unsigned int);

Quase lá. Tudo que temos a fazer agora é retomar as threads previamente suspensas da mesma maneira (iterando com Thread32First e Thread32Next com o mesmo handle instantâneo).

Não se esqueça de fechar 0 handle do processo vítima com CloseHandle() 😉

Shellcode

Depois de tudo isso, o fluxo de execução na thread selecionada do processo vítima atinge nosso shellcode (?), onde o código fonte deve estar muito claro agora. Ele simplesmente chama o LoadLibraryA () da API com o nome/caminho da DLL que queremos injetar.

Uma nota importante – é uma má prática fazer qualquer coisa “séria” dentro da função DllMain(). Minha sugestão é criar uma nova thread na DllMain() e fazer todo o trabalho lá, de modo que ele poderá retornar com segurança.

Espero que este artigo tenha sido útil.

Divirta-se injetando e vejo vocês na próxima.

***

Texto original disponível em http://syprog.blogspot.com.br/2012/05/createremotethread-bypass-windows.html