.NET

27 ago, 2012

Como fazer a injeção de código executável de uma maneira interessante

100 visualizações
Publicidade

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 & 0x20000020) = 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 0x200 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 disponível em: http://syprog.blogspot.com.br/2011/12/executable-code-injection-interesting.html