▸ Payload Encryption — 4/1

AES Encryption para Payloads — BCrypt vs Tiny-AES e o Problema da IAT

AES é o padrão de criptografia simétrica mais robusto que você vai usar pra proteger payloads em memória. Se você já passou por XOR e RC4, agora o jogo muda: chave de 256 bits, IV obrigatório, modo CBC com encadeamento de blocos. Mas a pergunta que importa não é "como criptografar" — é qual abordagem usar sem entregar seu binário de bandeja pra soluções de segurança.

Como o AES funciona por baixo

AES (Advanced Encryption Standard) é uma cifra de bloco simétrica — ou seja, usa a mesma chave pra criptografar e descriptografar, e opera em blocos fixos de 16 bytes. O modo que a gente usa aqui é o CBC (Cipher Block Chaining): cada bloco de plaintext sofre XOR com o bloco cifrado anterior antes de ser criptografado. O primeiro bloco usa o IV (Initialization Vector) — um vetor de 16 bytes que garante que dois plaintexts iguais, com a mesma chave mas IVs diferentes, produzam ciphertexts completamente diferentes.

O ponto aqui é: o IV não é segredo, mas precisa ser único por operação. Reusar IV com a mesma chave destrói a segurança do CBC — permite ataques de análise de padrões nos blocos. Na prática, pra payloads, você gera IV aleatório junto com a chave no momento da criptografia e embute ambos no loader.

AES-256 usa chave de 32 bytes e opera em 14 rounds de substituição (S-Box — tabela de substituição não-linear que dificulta ataques algébricos), permutação de linhas, mistura de colunas e adição de subchave. Isso é o que torna brute-force inviável.

Abordagem 1: BCrypt WinAPI (CNG)

A primeira abordagem usa a BCrypt API, parte do CNG (Cryptographic Next Generation) — a API nativa do Windows pra criptografia moderna. A vantagem: suporte completo a padding PKCS#7 (esquema que preenche o último bloco com bytes indicando quantos bytes faltam — se faltam 5 bytes, preenche com 0x05 cinco vezes), gerenciamento de chave pelo OS e código relativamente direto.

A struct central organiza tudo:

C
12345678
typedef struct _AES {
    PBYTE   pPlainText;      // Dados em texto claro
    DWORD   dwPlainSize;     // Tamanho do plaintext
    PBYTE   pCipherText;     // Dados criptografados
    DWORD   dwCipherSize;    // Tamanho do ciphertext (pode ser maior por padding)
    PBYTE   pKey;            // Chave de 32 bytes
    PBYTE   pIv;             // IV de 16 bytes
} AES, *PAES;

O fluxo de InstallAesEncryption segue o padrão BCrypt:

C
12345678910111213141516171819202122232425
// 1. Abre handle do algoritmo AES
BCryptOpenAlgorithmProvider(&hAlgorithm, BCRYPT_AES_ALGORITHM, NULL, 0);

// 2. Obtém tamanho do key object (buffer interno que o BCrypt precisa)
BCryptGetProperty(hAlgorithm, BCRYPT_OBJECT_LENGTH, (PBYTE)&cbKeyObject, ...);

// 3. Verifica block size — deve ser 16
BCryptGetProperty(hAlgorithm, BCRYPT_BLOCK_LENGTH, (PBYTE)&dwBlockSize, ...);

// 4. Configura modo CBC
BCryptSetProperty(hAlgorithm, BCRYPT_CHAINING_MODE, 
    (PBYTE)BCRYPT_CHAIN_MODE_CBC, sizeof(BCRYPT_CHAIN_MODE_CBC), 0);

// 5. Gera chave simétrica
BCryptGenerateSymmetricKey(hAlgorithm, &hKeyHandle, pbKeyObject, 
    cbKeyObject, (PBYTE)pAes->pKey, KEYSIZE, 0);

// 6. Primeira chamada — descobre tamanho do output
BCryptEncrypt(hKeyHandle, pAes->pPlainText, pAes->dwPlainSize, 
    NULL, pAes->pIv, IVSIZE, NULL, 0, &cbCipherText, BCRYPT_BLOCK_PADDING);

// 7. Aloca buffer e executa a criptografia de fato
BCryptEncrypt(hKeyHandle, pAes->pPlainText, pAes->dwPlainSize, 
    NULL, pAes->pIv, IVSIZE, pbCipherText, cbCipherText, &cbResult, 
    BCRYPT_BLOCK_PADDING);

A flag BCRYPT_BLOCK_PADDING é crucial — ela diz pro BCrypt aplicar PKCS#7 automaticamente. Sem ela, se seu payload não for múltiplo de 16 bytes, a chamada falha. A descriptografia (InstallAesDecryption) é praticamente idêntica, trocando BCryptEncrypt por BCryptDecrypt.

O padrão de duas chamadas (primeira pra obter tamanho, segunda pra executar) é idiomático da BCrypt API. Parece verboso, mas garante que você aloca exatamente o necessário.

IAT do binário usando BCrypt — funções de criptografia visíveis

O problema real: a IAT

E aqui tá o ponto que muda tudo na prática. Quando você usa BCrypt, funções como BCryptOpenAlgorithmProvider, BCryptEncrypt, BCryptDecrypt, BCryptGenerateSymmetricKey ficam expostas na IAT (Import Address Table) — a tabela que lista todas as funções importadas de DLLs externas no binário PE. Qualquer solução de segurança que escaneie a IAT vai ver um binário importando a stack completa de criptografia do Windows e pode classificar como suspeito imediatamente.

Na minha visão, pra payloads de red team, usar BCrypt direto sem esconder as importações é quase um atestado de detecção. Funciona bem pra entender o mecanismo, mas em produção você precisa de IAT hiding — seja via dynamic resolution com GetProcAddress, syscalls indiretos, ou API hashing. Isso é assunto pra outro módulo, mas o ponto é: entender a IAT é tão importante quanto entender a criptografia em si.

Abordagem 2: Tiny-AES-C (Biblioteca Portátil)

A alternativa é a Tiny-AES-C — uma biblioteca open-source, portátil e minimalista que implementa AES em C puro, sem dependência de nenhuma API do Windows. Você inclui aes.h e aes.c no projeto e pronto.

Por padrão ela usa AES-128. Pra AES-256, você define #define AES256 1 no header. O uso é bem mais enxuto:

C
123456789
#include "aes.h"

struct AES_ctx ctx;
// Inicializa com chave e IV
AES_init_ctx_iv(&ctx, pKey, pIv);
// Criptografa in-place (modifica o buffer diretamente)
AES_CBC_encrypt_buffer(&ctx, PaddedBuffer, PaddedSize);
// Descriptografa in-place
AES_CBC_decrypt_buffer(&ctx, CipherText, sizeof(CipherText));

Três linhas e tá criptografado. Mas tem uma pegadinha importante: Tiny-AES não suporta padding. Se seu buffer não for múltiplo de 16, você precisa resolver isso antes.

A solução é uma função de padding manual:

C
1234567891011121314
BOOL PaddBuffer(IN PBYTE InputBuffer, IN SIZE_T InputBufferSize, 
    OUT PBYTE* OutputPaddedBuffer, OUT SIZE_T* OutputPaddedSize) {

    SIZE_T PaddedSize = InputBufferSize + 16 - (InputBufferSize % 16);
    PBYTE PaddedBuffer = (PBYTE)HeapAlloc(GetProcessHeap(), 0, PaddedSize);
    if (!PaddedBuffer) return FALSE;

    ZeroMemory(PaddedBuffer, PaddedSize);
    memcpy(PaddedBuffer, InputBuffer, InputBufferSize);

    *OutputPaddedBuffer = PaddedBuffer;
    *OutputPaddedSize = PaddedSize;
    return TRUE;
}

O cálculo InputBufferSize + 16 - (InputBufferSize % 16) encontra o próximo múltiplo de 16. O ZeroMemory zera o buffer inteiro antes de copiar — os bytes extras ficam como 0x00. Não é PKCS#7 formal (que preencheria com o valor do número de bytes faltantes), mas funciona pra payloads onde você conhece o tamanho original.

IAT usando Tiny-AES — sem funções de criptografia do Windows

A vantagem real: IAT limpa

Com Tiny-AES, nenhuma função de criptografia aparece na IAT. A implementação é compilada diretamente no binário como código estático. Do ponto de vista de um scanner de IAT, seu binário não importa nada relacionado a criptografia — o que reduz significativamente a superfície de detecção estática.

Análise: Onde Cada Abordagem Falha

BCrypt WinAPI:

  • Prós: padding automático, manutenção pelo OS, modo FIPS se necessário

  • Contras: IAT exposta, mais verboso, dependência de bcrypt.dll

  • Detecção: ETW (Event Tracing for Windows — sistema de telemetria do kernel que pode registrar chamadas a APIs de criptografia) pode logar chamadas BCrypt. Soluções como Sysmon com regras customizadas podem correlacionar um processo desconhecido importando a stack completa de BCrypt com comportamento suspeito

Tiny-AES-C:

  • Prós: IAT limpa, portátil, zero dependência de API

  • Contras: sem padding nativo, arrays da biblioteca podem estar assinados

  • Detecção: análise estática de bytes pode detectar as S-Boxes e tabelas de lookup do AES compiladas no binário. Soluções mais avançadas fazem pattern matching em constantes criptográficas conhecidas

Sobre a detecção das constantes do Tiny-AES — uma alternativa que descobri é aplicar XOR nos vetores e tabelas internas da biblioteca e descriptografar em runtime antes de inicializar. Adiciona complexidade, mas quebra as assinaturas estáticas.

Na Minha Visão

Pra desenvolvimento de payloads, Tiny-AES é a escolha pragmática na maioria dos cenários. A IAT limpa é uma vantagem enorme que supera a falta de padding — resolver padding manualmente é trivial. BCrypt faz sentido quando você já tem IAT hiding implementado ou quando precisa de compliance/FIPS.

O que eu acho subestimado nessa discussão é o impacto da IAT na triagem automatizada. Muita gente foca em ofuscação de strings, entropy do binário, sandbox evasion — mas esquece que a IAT é literalmente a primeira coisa que um analista ou ferramenta olha. Um binário com BCryptEncrypt + VirtualAlloc + CreateRemoteThread na IAT é praticamente um alerta ambulante.

A geração de chave e IV usando rand() com srand(time(NULL)) é aceitável pra lab e demonstração, mas em produção é previsível. Um atacante que conheça o timestamp aproximado da compilação/execução pode reconstruir a chave. Pra cenários reais, use BCryptGenRandom ou RtlGenRandom — ironicamente, isso reintroduz dependência de API, então o trade-off de detecção precisa ser avaliado caso a caso.

Próximos Passos

O caminho natural depois de dominar AES pra payloads é: IAT hiding (resolver o problema principal do BCrypt), API hashing (resolver importações de qualquer API sensível), e criptografia de payload em disco vs em memória (quando descriptografar, onde manter a chave). Também vale estudar modos alternativos como AES-CTR (que não precisa de padding) e técnicas de key derivation com HKDF pra não embarcar a chave raw no binário.

Monte um lab comparativo: compile o mesmo payload com BCrypt e Tiny-AES, jogue no VirusTotal ou em análise local com PE-bear/CFF Explorer, e compare as IATs. Esse exercício vale mais que qualquer tutorial.