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.

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:
inByteselength- os bytes da dll e o tamanho em bytesuserFunction,userDataeuserLength- é 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 READMEoutByteseoutLength- 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

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

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.

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:

Referencias
- https://web.archive.org/web/20191212072026/https://silentbreaksecurity.com/srdi-shellcode-reflective-dll-injection/
- https://web.archive.org/web/20230328191606/https://www.netspi.com/blog/technical/adversary-simulation/srdi-shellcode-reflective-dll-injection/
- https://www.outflank.nl/blog/2019/06/19/red-team-tactics-combining-direct-system-calls-and-srdi-to-bypass-av-edr/
- https://web.archive.org/web/20171118102938/https://countercept.com/our-thinking/doublepulsar-usermode-analysis-generic-reflective-dll-loader/
- https://web.archive.org/web/20130921213639/http://www.exploit-monday.com/2013/08/writing-optimized-windows-shellcode-in-c.html
- https://disman.tl/2015/01/30/an-improved-reflective-dll-injection-technique.html
- https://www.rbtsec.com/blog/shellcode-reflective-dll-injection-srdi-converting-dlls-into-position-independent-shellcode/
- https://www.outflank.nl/blog/2019/06/19/red-team-tactics-combining-direct-system-calls-and-srdi-to-bypass-av-edr/
- https://gatari.dev/posts/long-live-the-shellcode/#shellcode-reflective-dll-injection-srdi