Geleia Cybersecurity professional

Shellcode RDI

2026-07-31

Shellcode Reflective Dll Injection (sRDI)

sRDI é uma tecnica que tem a proposta de ser uma versão melhorada da tecnica classica de Reflective DLL Injection. Segundo o autor Nick Landers monoxgas a ideia é diminuir o tamanho do código de Reflective Injection e fazer com que qualquer dll seja carregada e executada em memória sem necessidade de ter uma dll reflectiva.

A motivação pra isso é que as POCs que utilizam a tecnica RDI precisam “entender” o RDI, ou seja, tanto o Injector quanto a Dll a ser injetada precisam estar preparados um para o outro, isso deixa o código marcado com uma assinatura especifica e fica facil de detectar.

Para solucionar isso foi criado a suite sRDI que é um conjunto de “ferramentas” que possibilitam usar e até modificar essa tecnica de forma que precisar.

Suite sRDI

Esse projeto tem varios programas e scripts diferentes, então pra não ficar um texto muito longo vou focar nos mais importantes para o entendimento da tecnica:

  • ShellcodeRDI: contém o código responsável pelo reflective injection, quando compilado gera um arquivo PE e sera convertido em shellcode que faz parte do loader final.

  • NativeLoader: loader final que recebe a dll a ser injetada e ja tem toda a logica da tecnica sRDI.

  • Python\EncodeBlobs.py: esse script faz a conversão do binario ShellcodeRDI para o shellcode estatico que é usado no loader final.

ShellcodeRDI

O código presente no ShellcodeRD\ShellcodeRDI.c, grosseiramente dizendo, tem a mesma funcionalidade de Reflective Loading da poc original mas com algumas mudanças que deixaram o código menor, position independent e um pouco mais stealth.

ULONG_PTR LoadDLL(PBYTE pbModule, DWORD dwFunctionHash, LPVOID lpUserData, DWORD dwUserdataLen, PVOID pvShellcodeBase, DWORD dwFlags)

A definição da função é simples, pbModule recebe um ponteiro para a dll em memória e os outros argumentos são usados para execução de alguma função exportada pela dll.

A baixo estão os steps do código de acordo com os comentarios:

step 1 - Function Call Obfuscation: resolve os endereços das funções necessarias, como VitualAlloc, VirtualProtect e LoadLibrary, que seram usadas depois. A obfuscação acontece porque os endereços das funções são resolvidos em runtime e usados atravez de ponteiros para esse endereço, então ao invéz de chamar a função para o S.O reolver, o endereço é resolvido manualmente em uma dll ja carregada no processo atual.

Isso é possível percorrendo a memória do processo atual a partir do PEB. O PEB guarda informações como os modulos (DLLs) carregadas na memória e a partir dos modulos é possível chegar até as funções exportadas pelas dlls. Fazendo isso o binario final não importa nenhuma função suspeita deixando a IAT “limpa” de certa forma.

step 2 - Aloca memória e copia os headers da dll: aloca memória para receber os bytes da dll. O tamanho é baseado no campo ImageBase que é o tamanho do dll em bytes. E depois copia os Headers da dll para a memória alocada.

step 3 - Copia as Sections Headers: Copia todas as sections da dll para a memória alocada.

step 4 - Processa as Image Base Relocations.

step 5 e 6 - Processa a Import Table: Aqui caso a dll importe alguma função ela será inserida na memória alocada.

step 7 - Permissões de memória: Aqui é setado as permissões de memória de acordo com as permissões da dll (Não tenho certeza do porque, já que isso não faz parte da poc original).

step 8 - Executa os TLS Callbacks: Se a dll tiver alguma função TLS callback ela é encontrada e executada.

step 9 - É uma função usada para lidar com possiveis erros no registradores, ja que uma parte da poc usa opcodes assembly que vão ser passados manualmente e fazem um alinhamento da stack.

step 10 - Executa o Entrypoint da DLL

step 11 - Por ultimo executa alguma função exportada caso exista.

Compile ShellcodeRDI

O build do projeto é praticamente todo automatizado com os PostBuildEvent que Visual Studio oferece. Depois de compilar o código um evento é acionado executando o script powershell lib\PowerShell\Out-Shellcode.ps1 que extrai a sessão .text do PE e armazena os bytes (raw format) na arquitetura de 32 e 64 bits salvando num aquivo em \bin\ShellcodRDI_x64 e x86.

<PostBuildEvent>
      <Command>powershell.exe -NoProfile -ExecutionPolicy Bypass -File "$(SolutionDir)lib\PowerShell\Out-Shellcode.ps1" "$(OutDir)$(TargetName)$(TargetExt)" "$(OutDir)$(TargetName).map" "$(SolutionDir)bin\$(TargetName)_x86.bin"</Command>
    </PostBuildEvent>
    <PostBuildEvent>
      <Message>Extract position independent shellcode</Message>
    </PostBuildEvent>

Para fazer o parse e extrair a .text do binario foi usado Get-PEHeader.ps1 script de uma versão mais antiga do PowerSploit.

Em resumo, o build do projeto envolve o uso de scripts para automatizar as coisas, então caso queira modificar o código vai precisar fazer algumas coisas manualmente.

Uma dessas é buildar o ShellcodeRDI para as duas arquiteturas x86 e x64, para a x86 use a que ja vem configurado no projeto.

  • Obs: para gerar arquivos .bin nas duas arquiteturas faça o build somente do ShellcodeRDI, não do projeto todo, apenas mude para Win32 ou x64 e build.

srdbuild

Build static shellcode RDI

Depois de buildar o ShellcodeRDI e gerar os arquivos .bin, a suite oferece um script python que converte o shellcode em raw format para hexadecimal que será usado no loader final.

O script lib\Python\EncodeBlobs.py recebe o path principal do projeto e ja faz a alteração nos arquivos necessarios.

NativeLoader

NativeLoader implementa funções que torna possivel executar o shellcodeRDI carregando e executando uma dll em memória.

O código possui uma função principal que é reponsavel por agrupar o shellcodeRDI, os bytes da DLL e o bootstrap que são alguns bytes (opcodes) que vem antes de tudo e é reponsavel por passar os argumentos e alinhar a stack para a função do shellcodeRDI que veremos a seguir.

NativeLoader main function

A função main começa parseando os argumentos passados para o programa que precisa apenas do path de uma DLL ou de um arquivo .bin para realizar a injeção.

Ao receber o arquivo ele é passado para função GetFileContents que recebe e faz o parse e salva os bytes e o tamanho do arquivo.

DWORD GetFileContents(LPCSTR filename, LPSTR *data, DWORD &size)

Depois de salvar os bytes do arquivo uma verificação é feita para saber se é um PE ou não analisando os dois primeiros bytes correspondem a MZ, que é a assinatura do formato PE (aka Magic Bytes).

if (data[0] == 'M' && data[1] == 'Z') {
		printf("[+] File is a DLL, attempting to convert\n");

		if (!ConvertToShellcode(data, dataSize, HashFunctionName("SayHello"), "dave", 5, SRDI_CLEARHEADER, finalShellcode, finalSize)) {
			printf("[!] Failed to convert DLL\n");
			return 0;
		}

Se o arquivo for um PE a execução é passada para a função ConvertToShellcode responsavel por montar “blob” que vai executar a tecnica sRDI.

  • Obs: a execução só vai pra essa função se for passado um arquivo PE, ja a outra opção é converter a DLL para um shellcode PIC com os scripts presentes no projeto, e que pode ser executado pelo NativeLoader também.

ConvertToShellcode function

Aqui nessa função que a magica acontece, primeiro vemos a definição da função que recebe os seguintes argumentos:

  • inBytes e length - os bytes da dll e o tamanho em bytes
  • userFunction, userData e userLength - é o nome de uma função exportada na dll que será executada depois de DllMain junto com algum argumento adicional.
  • flags - controlam a logica de loading, descrição presente no README
  • outBytes e outLength - são variaveis de output que recebem o blob final e seu tamanho.
BOOL ConvertToShellcode(LPVOID inBytes, DWORD length, DWORD userFunction, LPVOID userData, DWORD userLength, DWORD flags, LPSTR &outBytes, DWORD &outLength)
{

	LPSTR rdiShellcode = NULL;
	DWORD rdiShellcodeLength, dllOffset, userDataLocation;


	  //MARKER:S
    LPSTR rdiShellcode32 = "\x81\xEC\x14\x01\x00\x00 ...";
    LPSTR rdiShellcode64 = "\x48\x8B\xC4\x48\x89\x58 ...";
    DWORD rdiShellcode32Length = 2981, rdiShellcode64Length = 2772;
    //MARKER:E

Logo abaixo vemos os ShellcodeRDI nas variaveis rdiShellcode64 ou 32 que seram adicionados no blob final seguindo a arquitetura.

if (Is64BitDLL((UINT_PTR)inBytes))
	{

A arquitetura é verificada antes de iniciar a montagem do blob pela função Is64BitDLL que recebe os bytes da dll e compara o campo Magic do Optional Header que define a arquitetura. Se a dll for 64 bits o blob é montado de um jeito e se for 32 bits de outro.

Bootstrap + ShellcodeRDI + DLL

Seguindo com a montagem do blob temos o bootstrap que são uma serie de opcodes assembly, montados byte por byte, reponsavel por passar os argumentos para a função do ShellcodeRDI.

A maior parte do bootstrap ja é comentado então não vou prolongar muito. A passagem de argumento é feita pelos registradores e movendo os valores com MoveMemory para que possam ser manipulados. No final do bootstrap a execução é passada para o ShellcodeRDI, nesse momento os argumentos necessarios ja estão nos registradores e seram usados pela próxima função.

// call - Transfer execution to the RDI
		bootstrap[i++] = 0xe8;
		bootstrap[i++] = sizeof(bootstrap) - i - 4; // Skip over the remainder of instructions
		bootstrap[i++] = 0x00;
		bootstrap[i++] = 0x00;
		bootstrap[i++] = 0x00;

Por ultimo a montagem do blob é finalizada juntando todas as peças, primeiro o bootstrap, seguido pelo ShellcodeRDI e por ultimo os bytes da DLL junto com UserData.

	// Ends up looking like this in memory:
	// Bootstrap shellcode
	// RDI shellcode
	// DLL bytes
	// User data
	outLength = length + userLength + rdiShellcodeLength + sizeof(bootstrap);
	outBytes = (LPSTR)malloc(outLength);
	MoveMemory(outBytes, bootstrap, sizeof(bootstrap));
	MoveMemory(outBytes + sizeof(bootstrap), rdiShellcode, rdiShellcodeLength);
	MoveMemory(outBytes + sizeof(bootstrap) + rdiShellcodeLength, inBytes, length);
	MoveMemory(outBytes + sizeof(bootstrap) + rdiShellcodeLength + length, userData, userLength);

Para demonstração coloquei um printf na variavel que armazena o blob final, então na memória fica dessa forma:

1. - A parte em amarelo é o bootstrap

2. - Da parte azul em diante é o ShellcodeRDI

blob1

3. - Depois do sRDI vemos a dll em laranja começando com o MZ

blob2

Execute blob

Depois do blob montado na memória, o endereço é salvo na variavel rdi e em seguida é executado como uma função. Isso é possivel porque a permissão da memória é setada para RWX e o endereço aponta para o inicio dos opcodes, então a execução inicia sem a necessidade de criar uma thread por exemplo.

	RDI rdi = (RDI)(finalShellcode);

	printf("[+] Executing RDI\n");
 	HMODULE hLoadedDLL = (HMODULE)rdi(); // Excute DLL

E por fim, a execução do RDI chama a função main da dll, mas o código adiciona a possibilidade executar uma função exportada da dll também. Na ultima parte do código a função GetProcAddressR é usada para percorrer os headers da dll para encontrar alguma função exportada e executa-la.

	Function exportedFunction = (Function)GetProcAddressR(hLoadedDLL, "Uninstall");
	if (exportedFunction) {
		printf("[+] Calling exported functon\n");
		exportedFunction();

Running poc

Agora que sabemos a teoria vamos para a pratica. Para a demo vou usar uma dll gerada pelo msfvenom e mostrar como o blob gerado fica quando é executado:

1. - Sublinhado de vermelho vemos “call next instruction” que é o inicio do blob, aqui ele faz um call para o endereço 144A5134EA6 que está um pouco abaixo em um offset diferente.

2. - Sublinhado de azul temos o call que transfere a execução para o ShellcodeRDI, da mesma forma um call para o endereço 144A5116085 que é o inicio do shellcode um pouco mais abaixo.

blob3

Acontence muito mais depois disso então a ideia foi só mostrar como se parace quando é executado. No resultado final usei uma dll que executa a calculadora para demonstrar:

srdigif2

Referencias


Similar Posts

Artigo anterior Syscalls Hell's

Content