Um dos principais mecanismos de realizar-se uma comunicação entre processos (IPC) no Windows é através da memória compartilhada entre esses processos.

Sabendo disso, pipes são nada mais que um range de memória compartilhada tratadas como objeto de arquivo. Então, como são tratados como arquivos, podemos utilizar funções comuns destinadas a arquivos da Windows API como forma de interação para com esses pipes.

Podemos ler um pipe com a API ReadFile(), ou escrever no mesmo com a API WriteFile().


O modelo utilizado por pipes para manusear essas estruturas de dados é o FIFO (First in First out), ou seja, o primeiro byte escrito no pipe será o primeiro a ser lido nesse pipe, e, após ser lido, esse byte ficará indisponível para ser lido de novo.

No entanto, a API PeekNamedPipe() contorna essa situação lendo dados de um pipe sem removê-los.


Existem dois principais tipos de pipes: Named pipes e Anonymous pipes. A diferença entre eles é que um recebe um nome, e o outro não.


Neste artigo, vamos focar principalmente nos named pipes, porém alguns conceitos explicados aqui também servem para anonymous pipes.

Um named pipe funciona através de um modelo client-server, onde há o named pipe servidor e diversos named pipes clientes. Todas as instâncias de um named pipe específico devem compartilhar o mesmo nome, porém cada instância tem seu próprio buffer, handle e canal de comunicação.


Agora, como é feita a comunicação entre esses pipes?

Sabendo que esse mecanismos utiliza do modelo client-server, named pipes podem adotar duas formas de comunicação: half-duplex (HDX) ou full-duplex (FDX). No HDX, o cliente abre um canal onde pode escrever dados no servidor, sem resposta do servidor. Já no FDX, um canal de duas-mãos é aberto, possibilitando a escrita de ambos os lados.


Quando se trata de processos, qualquer processo pode acessar um named pipe, podendo agir tanto como servidor quanto cliente. A partir disso, a comunicação peer-to-peer se torna possível. O termo “server” de um named pipe, refere-se ao processo que o criou, enquanto o termo “client”, refere-se ao processo conectado ao processo servidor.

Named pipes, além de possibilitarem a IPC entre processos locais, também permitem a comunicação entre processos de hosts remotos. Quando um processo servidor está rodando, todos seus named pipes são acessíveis remotamente também.


Para o processo servidor criar um named pipe, utiliza-se a API CreateNamedPipe(), e para aceitar uma conexão do processo cliente, usa-se a API ConnectNamedPipe(). Já pelo client side, nos conectamos a um named pipe com as APIs CreateFile() ou CallNamedPipe().

Podemos verificar as permissões de um named pipe em específico com a ferramenta accesschk.exe do conjunto Sysinternals, passando “pipe” como primeiro argumento - accesschk.exe pipe.


Named pipes também possibilitam serem utilizados de forma maliciosa por atacantes. Um atacante pode usar desse mecanismo para movimento lateral em um host já comprometido, escalar seus privilégios e até mesmo em malwares, tonando-os uma stream de dados alternativa.

Temos como exemplo o ransomware NotPetya (ou Petrwrap) onde um de seus TTPs, o roubo de credenciais, era feito através de named pipes.


NotPetya Ransomware:

O ransomware NotPetya comunicava-se via named pipes com o módulo de roubo de credenciais do sistema da vítima.

Esse named pipe era criado a partir de um GUID aleatório. Onde o módulo de roubo de credenciais era lançado como um child process dessa GUID, levando o named pipe como argumento.

Uma vez que carregado, o módulo de roubo de credenciais faz uma chamada a API OpenProcess() no lsass.exe com a flag de acesso configurado em VM_READ, então, procura pelo módulos wdigest.dll e lsasrv.dll, ambos usados para autenticação de usuários no sistema.


Concluindo, o módulo extrai credenciais do LSASS (Igualmente ao Mimikatz) e as envia ao processo do malware NotPetya através de um named pipe.


Agora que temos uma pequena noção sobre como os named pipes funcionam, e como podem ser implementados em tarefas maliciosas, vamos aplicar esse conhecimento.

Disclaimer: A sample em questão trata-se de um script malicioso em powershell severamente ofuscado, do qual o processo de desofuscação não pertence ao escopo do artigo. Dito isso, partiremos para a análise do shellcode já desofuscado e o uso dos named pipes no mesmo.


Ao chegarmos no shellcode desofuscado, encontramos o seguinte:

Podemos dumpar esse shellcode com a opção “Save output to file”, do próprio cyberchef.

Ao abrir o binário no DiE, não encontramos nenhuma informação útil. Afinal, trata-se de um shellcode.

Nossa opção agora é: injetar esse shellcode em algum processo. A injeção deve ser feita em um binário qualquer de 32-bits, já que o shellcode é feito na arquitetura 32-bits.

Injeção do shellcode:

A técnica a seguir pode ser usada não somente para injetar shellcodes/payloads, mas qualquer injeção em geral.

Começamos abrindo um processo de 32-bits qualquer no x64dbg. Após aberto, precisamos primeiro alocar memória para esse shellcode, só então prosseguir com a injeção propriamente dita. A Alocação de memória se dá ao acessar a aba “Memory Map”, clicando com o botão direito, selecionando a opção “Allocate Memory” e podemos deixar o valor padrão, já é o suficiente.

Após alocada, precisamos preencher essa memória com o shellcode. Abrimos o shellcode em algum editor hexadecimal de sua preferência, selecionamos todos seus bytes e os copiamos.

Na imagem utilizo um editor que gosto bastante: o ImHex, desenvolvido pelo @WerWolv. WerWolv/ImHex: 🔍 A Hex Editor for Reverse Engineers, Programmers and people who value their retinas when working at 3 AM. (github.com)


Então, com esses bytes copiados, basta colá-los no nosso debugger.

Clicamos com o botão direito no primeito byte no dump da memória recém alocada, selecionamos “Binary”, então: Paste (Ignore Size).

Feito isso, teremos o seguinte resultado:

O próximo passo é a visualização desse conteúdo em instruções. Clicamos com o botão direito nesses bytes e seguimos no disassemble.

E por último, afim de começarmos a debuggar esse shellcode, precisamos setar uma nova origem para (EIP) a primeira instrução do shellcode. Para isso: botão direito no disassemble, selecione “Set New Origin Here”.

Caso tudo dê certo, agora estaremos aptos a iniciar o processo de debugging/reversing desse shellcode.


Debugging/Reversing

Geralmente, em shellcodes, vamos encontrar muitas operações matemáticas e desvios de fluxos repentinos, tenha isso em mente. A ideia aqui é entender quais syscalls são feitas pelo binário.


Logo de ínicio, descendo um pouco o disassemble, vemos um desvio de fluxo bastante interessante, há um jmp eax. Então vale a pena colocar um breakpoint nessa instrução.

Aperte F9 (Run). Ao chegarmos nesse breakpoint, percebemos que a instrução jmp eax, na verdade, é uma chamada para a API VirtualAlloc().

Checando os argumentos passados a API, vemos que:

É feita uma alocação de memória do tipo MEM_COMMIT, com 0x0007FFFF (524287) bytes de tamanho.

Estendendo a análise, percebemos que todo esse algoritmo trata-se de um loop, onde são passadas diversas APIs a esse jmp eax. Então vamos continuando a execução até parar na instrução jmp eax de novo.


O segundo hit do breakpoint já fica interessantíssimo, é uma chamada a API CreateNamedPipe(), tema principal deste artigo.

O named pipe:

Vendo os argumento passados à API, encontramos o nome do named pipe criado pelo shellcode, sendo ele: \\\\.\\pipe\\status_b5ba.

Para verificarmos a existência desse named pipe, poedmos acessar a aba handles, refresh e procuramos pelo nome do named pipe.

Perceba também que o “tipo” do named pipe está como um arquivo, assim como dito no começo deste artigo.

O terceiro hit no breakpoint da instrução jmp eax, é a ConnectNamedPipe(). API essa que habilita um named pipe no processo servidor a receber conexões de named pipes clientes. Podemos confirmar isso verificando os parâmetros passados a essa API, sendo o handle setado para 1A8, justamente o handle do named pipe criado.

Porém agora temos um problema. Após essa chamada, não temos mais hits no nosso breakpoint, o código permanece rodando eternamente.

De acordo com a documentação da Microsoft, caso o parâmetro hNamedPipe da API ConnectNamedPipe() não for aberto com a flag FILE_FLAG_OVERLAPPED (O que é o nosso caso), a função não retornará até receber uma conexão ou um erro ocorrer.


Sabendo disso, podemos inferir que o shellcode está esperando uma conexão no named pipe que o mesmo criou. E como prosseguimos a análise? Simples! Nos conectamos a esse named pipe!

Conexão:

O intuito agora é nos conectarmos a esse named pipe através da função CallNamedPipeA(). Para realizarmos essa conexão um simples código em C será o suficiente.

#include <windows.h>

int main (void) {

	CallNamedPipeA(
		"\\\\.\\pipe\\status_b5ba", //lpNamedPipeName
		(LPVOID)"ABCDEFGH",         //lpInBuffer
		8,                          //nInBufferSize
		NULL,                       //lpOutBuffer
		0,                          //nOutBufferSize
		NULL,                       //lpBytesRead
		NMPWAIT_NOWAIT              //nTimeOut
	);

	return 0;
}

Não esqueça de rodar o programa com os mesmos níveis de privilégios que o shellcode congelado no debugger.

Leitura:

Após a conexão ser feita, vemos que o shellcode resume sua execução, hitando novamente o breakpoint na instrução jmp eax, mas dessa vez, chamando a API ReadFile() (Utilizada também para ler o conteúdo desse named pipe, como dito anteriormente).


A ReadFile(), por sua vez, lê os 0004 primeiros bytes do buffer desse named pipe no endereço 0x00FFFAE0, podemos confirmar essa leitura seguindo esse endereço no dump.

Antes da chamada:

Depois da chamada:

Após essa chamada, outra é feita na mesma API, porém dessa vez lendo 2000 bytes.

Dessa forma, podemos entender que a primeira chamada a ReadFile() serve apenas como uma verificação.


Confirmamos isso olhando para o parâmetro nNumberOfBytesToRead da API.

Porém, essa chamada serve para ler o resto do buffer contido no named pipe. Ao verificarmos no dump desse endereço, encontramos o resto do nosso buffer.

Uma terceira chamada a essa API é realizada, lendo mais 2000 bytes.

Uso do Buffer:

Agora precisamos entender como é utilizado esse buffer. Para isso utilizaremos hardwares breakpoints no acesso (escrita ou leitura) desses bytes vindo de qualquer fonte externa.

Para setar hardware breakpoints: botão direito no primeiro byte do dump, breakpoint, hardware, access, byte.

Ao hitar nosso breakpoint, percebemos que [esp+8] é movido a ecx, caracterizando um acesso a esse endereço. Olhando o disassemble desse breakpoint, vemos que: caso ecx for igual a eax, ocorre um jmp para [esp+10].

[esp+10], por sua vez, refere-se a “segunda metade” do buffer armazenado pelo named pipe.


Então, caso ecx (ABCD em LE) for igual ao tamanho do parâmetro passado em nInBufferSize (0004 em LE), o jmp ocorreria para [esp+10] (“EFGH” (segunda metade do shellcode)), executando qualquer coisa que estiver nesse endereço.

Exemplo:

#include <windows.h>

int main (void) {

	unsigned char shellcode[] = "\x04\x00\x00\x00\x6e\x2f\x73\x68\x68\x2f\x2f\x62\x69\x89\xe3\x50\x89\xe2\x53\x89\xe1\xb0\x0b\xcd\x80..." //exemplo

	CallNamedPipeA(
		"\\\\.\\pipe\\status_b5ba", //lpNamedPipeName
		(LPVOID)shellcode,          //lpInBuffer
		4000,                       //nInBufferSize
		NULL,                       //lpOutBuffer
		0,                          //nOutBufferSize
		NULL,                       //lpBytesRead
		NMPWAIT_NOWAIT              //nTimeOut
	);

	return 0;
}

Onde, seriam comparados os primeiros \x04\x00\x00\x00 (nInBufferSize) e, caso fossem iguais, executaria o código contido a partir de \x6e\x2f\x73\x68....

Conclusão

Concluímos que: O binário inicial (shellcode) realiza uma injeção de código através do buffer de um named pipe. Esperando que o tamanho do shellcode seja definido nos primeiros parâmetro nInBufferSize. Sendo assim, o trabalho é distribuído entre dois binários, um injetando o código, e o outro guardando o código a ser injetado.


E é isso por enquanto pessoal, espero que tenham gostado. Uma ótima semana!

Leitura Complementar:

Identifying Named Pipe Impersonation and Other Malicious Privilege Escalation Techniques (securityintelligence.com)

FalconFriday — Suspicious named pipe events — 0xFF1B | by Olaf Hartong | FalconForce | Medium