Injeção de código executável: em geral, este termo está associado
com uma intenção maliciosa. Em muitos casos é verdade, mas em outros
não. Por ser pesquisador de malware durante a maior parte da minha
carreira, posso garantir que esta técnica parece ser muito útil quando
se pesquisa software malicioso, pois permite (na maioria dos casos)
vencer a sua proteção e reunir grande parte das informações necessárias.
Apesar de não ser recomendado usar essa abordagem, às vezes é
simplesmente inevitável.
Existem várias maneiras de realizar a injeção de código. Vamos dar uma olhada nelas.
Injeção de DLL
A maneira mais simples para se injetar uma DLL em outro processo é
criar um segmento remoto no contexto do processo, passando o endereço da
API LoadLibrary como um ThreadProc. No entanto, isso parece ser
confiável em versões modernas do Windows – devido à randomização do
endereço.
Outra maneira, um pouco mais complicada, implica em injetar um
códigoshell no espaço de endereço de outro processo e lançá-lo como uma
tarefa remota. Este método oferece uma maior flexibilidade e está
descrito aqui.
Mapeamento manual de DLL
Infelizmente, tornou-se moda dar novos nomes extravagantes para as
boas e velhas técnicas. O mapeamento manual de DLL nada mais é do que
uma complicada injeção de código. Complicada porque envolve a
implementação de um loader PE personalizado, que deve ser capaz de
resolver realocações. Aderindo o princípio da Navalha de Occam, assumo a
responsabilidade de afirmar que é muito mais fácil e faz mais sentido
simplesmente alocar a memória em outro processo usando VirtualAllocEx
API e injetar a posição do código shell independente.
Injeção de código de forma simples
Como o título desta seção já diz, esta é a maneira mais simples.
Atribua alguns blocos de memória no espaço de endereço do processo
remoto usando VirtualAllocEx (um para o código e outro para os dados),
copie o código shell e seus dados para os blocos e lance-os como um
thread remoto.
Todos os métodos mencionados acima são bem abrangidos na Internet.
Você pode pesquisar apenas por “injeção de código” e terá milhares de
tutoriais e artigos bem escritos. Minha intenção é descrever uma maneira
mais interessante e, também, mais complexa de injeção de código (na
esperança de que você não tenha mais nada a fazer senão tentar
implementá-lo).
Antes de começar, outra nota para os nerds:
- O código neste artigo não contém as verificações de segurança – a menos quando for necessário como um exemplo;
- Isto não é um artigo sobre a escrita de malware; então não ligo se o AV dá um alerta quando tentamos usar este método;
- Não,o mapeamento DLL manual não é melhor;
- Nem me preocupo se esta solução é estável. Se decidir implementar, será em seu próprio risco.
Agora, vamos nos divertir!
Disk versus layout de memória
Antes de prosseguir com a explicação, vamos dar uma olhada no layout
do arquivo PE, seja em disco ou memória, já que a nossa solução se
baseia nisso.

Este layout é logicamente idêntico para ambos os arquivos PE no disco e
na memória. As únicas diferenças são que algumas partes podem não estar
presentes na memória e, o mais importante para nós, nos disco os artigos
são alinhados por “alinhamento de arquivo”, enquanto os da memória são
alinhados por “página de alinhamento”; valores que, por sua vez, podem
ser encontrados no cabeçalho opcional. Confira aqui a referência completa de formato COFF PE.
Agora estamos particularmente interessados em seções que contenham o código executável ((SectionHeader.characteristics & 0×20000020) = 0).
Normalmente, o código atual não ocupa toda a seção, deixando algumas
partes preenchidas apenas por zeros. Por exemplo, se a nossa seção de
código contém apenas “ExitProcess (0) ‘, que pode ser compilado
em oito bytes, ainda ocupará bytes de FileAlignment no disco
(normalmente 0×200 bytes). Isso ocupará ainda mais espaço na memória e o
mais perto que se pode mapear da seção seguinte é this_section_virtual_address + PageAlignement
(neste caso em particular). Isso significa que se tivermos 0x1F8 bytes
livres quando o arquivo estiver no disco, teremos 0xFF8 bytes livres
quando o mesmo for carregado na memória.
A “fórmula” para calcular o espaço disponível na seção de código é next_section_virtual_address – (+ this_section_virtual_address this_section_virtual_size)
porque o tamanho virtual é (geralmente) a quantidade de dados reais da
seção. Lembre-se disto, já que é o espaço que vamos utilizar como nossa
meta de injeção.
Pode acontecer do executável não ter espaço livre o suficiente para o
nosso código shell, mas não deixe que isso te incomode. Um processo
contém mais de um módulo (o executável principal e todos os DLLs). Isso
significa que você pode procurar o espaço livre nas seções de código de
todos os módulos. Por que apenas nas seções de código? Só para não mexer
muito com a proteção de memória.
Códigos shell
A primeira e mais importante regra para códigos shell é que eles
devem ser independentes da posição. No nosso caso, esta regra é
especialmente inevitável, pois vai ser espalhada por todo o espaço de
memória (depende do tamanho do seu código shell, é claro).
A segunda regra, mas não menos importante, é que você deve planejar
cuidadosamente o seu código de acordo com suas necessidades. Quanto
menos espaço ele ocupar, mais fácil será o processo de injeção.
Vamos manter o nosso código shell simples. Tudo o que ele vai fazer é
a intercepção de uma única API (não importa qual, selecione o que você
quiser da seção de importação de executáveis), e mostrar uma caixa de
mensagem cada vez que a API for chamada (você deve, provavelmente,
selecionar ExitProcess para a interceptação – caso não queira a caixa de
mensagem aparecendo o tempo todo).
Divida o seu código shell em blocos funcionais independentes. Quando
digo “independente”, quero dizer que ele não deve ter nenhuma chamada
diretas ou relativa ou obstáculos. Cada bloco deve possuir um campo de
dados, o qual conteria o endereço da tabela que contém os endereços de
todas as nossas funções e de dados (se necessário). Esse mecanismo
permitiria disseminar o código por todo o espaço disponível em
diferentes módulos, sem a necessidade de mexer com realocações.
As imagens abaixo te ajudarão a entender melhor o conceito.
Init
– é a nossa função de inicialização. Uma vez que o código for injetado,
você vai querer chamar essa função como um thread remoto.- Patch – este bloco é responsável por realmente juntar a tabela de importação com o endereço do nosso Fake.
O código em cada um dos blocos acima terá que acessar Data para recuperar os endereços de funções de outros blocos.
O seu procedimento de inicialização teria que relocar o KERNEL32.DLL
na memória, a fim de obter os endereços de LoadLibrary (sim, é melhor
usar o LoadLibrary do que o GetModuleHandle), GetProcAddress e funções
VirtualProtect API que são cruciais – mesmo para uma tarefa simples como
uma chamada API. Esses endereços seriam armazenados no Data.
O injetor
Enquanto o código shell é bastante trivial (pelo menos neste caso em
particular), o injetor não é. Ele não vai alocar a memória no espaço de
endereço de outro processo (se for possível, é claro). Ao invés disso,
ele vai analisar a PEB da vítima para obter a lista de módulos
carregados. Feito isso, ele analisa os cabeçalhos de seção de cada
módulo para criar uma lista de locais de memória disponível (lembre-se,
nós preferimos apenas as seções de código) e preenche o bloco de dados
com endereços apropriados. Vamos dar uma olhada em cada etapa.
Em primeiro lugar, pode ser uma boa ideia suspender o processo chamando a função SuspendThread em cada um dos seus threads. Talvez você queira ler este artigo
sobre enumeração de threads. Só mais uma coisa: abrir o processo de
vítima com as seguintes marcações: PROCESS_VM_READ |
PROCESS_VM_OPERATION | PROCESS_VM_WRITE | PROCESS_QUERY_INFORMATION |
PROCESS_SUSPEND_RESUME, para executar todas as operações seguintes. A
função em si é bem simples:
DWORD WINAPI SuspendThread(__in HANDLE hThread);
Não se esqueça de retomar os tópicos com ResumeThread quando a injeção for feita.
O próximo passo seria chamar a função NtQueryInformationProcess do ntdll.dll.
O único problema com isso é que ela não possui nenhuma biblioteca de
importação associada e você terá que localizá-la com o GetProcAddress
(GetModuleHandle (“ntdll.dll”), “NtQueryInformationProcess”). A menos
que você tenha uma maneira de especificá-la explicitamente na tabela de
importação de seu injetor. Além disso, tente LoadLibrary, caso o
GetModuleHandle não funcione para você.
NTSTATUS WINAPI NtQueryInformationProcess(
__in HANDLE ProcessHandle,
__in PROCESSINFOCLASS ProcessInformationClass, /* Use 0 in order to
get the PEB address */
__out PVOID ProcessInformation, /* Pointer to the PROCESS_BASIC_INFORMATION
structure */
__in ULONG ProcessInformationLength, /* Size of the PROCESS_BASIC_INFORMATION
structure in bytes */
__out_opt PULONG ReturnLength
);
typedef struct _PROCESS_BASIC_INFORMATION
{
PVOID ExitStatus;
PPEB PebBaseAddress;
PVOID AffinityMask;
PVOID BasePriority;
ULONG_PTR UniqueProcessId;
PVOID InheritedFromUniqueProcessId;
} PROCESS_BASIC_INFORMATION;
O NtQueryInformationProces te fornecerá o endereço do PEB do
processo. Este artigo vai te explicar como lidar com o conteúdo PEB.
Claro que você não será capaz de acessar diretamente o conteúdo (uma vez
que ele está no espaço de endereço de outro processo), assim você terá
que utilizar as funções WriteProcessMemory e ReadProcessMemory para isso.
BOOL WINAPI WriteProcessMemory(
__in HANDLE hProcess,
__in LPVOID lpBaseAddress, /* Address in another process */
__in LPCVOID lpBuffer, /* Local buffer */
__in SIZE_T nSize, /* Size of the buffer in bytes */
__out SIZE_T* lpNumberOfBytesWritten
};
BOOL WINAPI ReadProcessMemory(
__in HANDLE hProcess,
__in LPCVOID lpBaseAddress, /* Address in another process */
__out LPVOID lpBuffer, /* Local buffer */
__in SIZE_T nSize, /* Size of the buffer in bytes */
__out SIZE_T* lpNumberOfBytesRead
};
Devido ao fato de que você vai lidar com posições de memória somente para leitura, você deve chamar o VirtualProtectEx
a fim de tornar esses locais graváveis (PAGE_EXECUTE_READWRITE). Não se
esqueça de restaurar as permissões de acesso à memória para
PAGE_EXECUTE_READ quando tiver terminado.
BOOL WINAPI VirtualProtectEx(
__in HANDLE hProcess,
__in LPVOID lpAddress, /* Address in another process */
__in SIZE_T dwSize, /* Size of the range in bytes */
__in DWORD flNewProtect, /* New protection */
__out PDWORD lpflOldProtect
};
Você também pode querer mudar o VirtualSize dessas seções do
processo que você usou para injeção. Assim você poderá abranger o
código injetado. Basta ajustá-lo nos cabeçalhos na memória.
Isso é tudo pessoal! Permita-me deixar a parte mais difícil (escrever o código) para vocês.
Espero que este artigo tenha sido interessante e vejo vocês na próxima!
***
Artigo original de Alexey Lyashko, disponível em: http://syprog.blogspot.com.br/2011/12/executable-code-injection-interesting.html