Fuzzing em C com libFuzzer ou AFL

1. Introdução ao Fuzzing em C

Fuzzing é uma técnica de teste automatizado que consiste em fornecer entradas malformadas, aleatórias ou inesperadas a um programa, com o objetivo de provocar falhas como crashes, violações de memória ou comportamentos indefinidos. Em C, onde o gerenciamento manual de memória expõe vulnerabilidades graves (buffer overflow, use-after-free, double free), o fuzzing é uma prática essencial para segurança e robustez.

Existem três abordagens principais:
- Black-box fuzzing: o fuzzer não conhece a estrutura interna do programa.
- White-box fuzzing: o fuzzer tem acesso ao código-fonte e pode usar análise simbólica.
- Gray-box fuzzing: o fuzzer utiliza instrumentação leve para monitorar caminhos de execução e guiar a geração de entradas.

As ferramentas mais populares para fuzzing em C são:
- libFuzzer: fuzzer in-process, integrado ao LLVM, ideal para testes unitários e CI rápido.
- AFL (American Fuzzy Lop): fuzzer forkserver com instrumentação por compilador, excelente para fuzzing em larga escala.

2. Configuração do Ambiente e Instalação

Instalação do libFuzzer

O libFuzzer vem integrado ao Clang a partir da versão 6.0. Para verificar:

clang --version

Para compilar com libFuzzer, basta usar a flag -fsanitize=fuzzer:

clang -fsanitize=fuzzer,address -g -O1 fuzz_target.c -o fuzz_target

Instalação do AFL++

AFL++ é a versão mantida atualmente. Instalação via fonte:

git clone https://github.com/AFLplusplus/AFLplusplus.git
cd AFLplusplus
make
sudo make install

Ou via pacotes (Ubuntu/Debian):

sudo apt install afl++

Verifique a instalação:

afl-fuzz --version
afl-clang-fast --version

Sanitizers

Sempre compile com sanitizers para detectar bugs de memória:

-fsanitize=address,undefined

3. Escrevendo um Fuzz Target para libFuzzer

Um fuzz target para libFuzzer é uma função com a assinatura:

int LLVMFuzzerTestOneInput(const uint8_t *Data, size_t Size);

Exemplo prático: fuzzing de uma função que processa dados binários.

// fuzz_parser.c
#include <stdint.h>
#include <stddef.h>
#include <string.h>
#include <stdlib.h>

typedef struct {
    int id;
    char name[32];
    float value;
} Record;

int parse_record(const uint8_t *data, size_t size) {
    if (size < sizeof(Record)) return -1;
    Record *r = (Record *)data;
    if (r->id < 0) return -1;
    // Simula processamento
    char buffer[64];
    snprintf(buffer, sizeof(buffer), "ID: %d, Name: %.32s, Value: %f", r->id, r->name, r->value);
    return 0;
}

int LLVMFuzzerTestOneInput(const uint8_t *data, size_t size) {
    parse_record(data, size);
    return 0;
}

Compile e execute:

clang -fsanitize=fuzzer,address -g -O1 fuzz_parser.c -o fuzz_parser
./fuzz_parser

O libFuzzer gerará entradas aleatórias e reportará qualquer crash com o input que o causou.

4. Fuzzing com AFL: Instrumentação e Execução

Compilação com AFL

Use afl-clang-fast para instrumentar o código:

afl-clang-fast -fsanitize=address -g -O2 parser.c -o parser_afl

Preparação do corpus

Crie um diretório com entradas iniciais válidas:

mkdir input_corpus
echo "1,Joao,3.14" > input_corpus/seed1.txt
echo "2,Maria,2.71" > input_corpus/seed2.txt

Execução do AFL

afl-fuzz -i input_corpus -o output_corpus -- ./parser_afl

A interface TUI do AFL mostra:
- cycles done: número de iterações completas
- total paths: caminhos únicos descobertos
- crashes: número de entradas que causaram crash
- timeouts: entradas que excederam o tempo limite

Exemplo: fuzzing de parser JSON

// json_parser.c
#include <stdio.h>
#include <string.h>
#include <stdlib.h>

int parse_json(const char *input) {
    // Parser extremamente simples e inseguro
    char stack[256];
    int top = -1;
    while (*input) {
        if (*input == '{' || *input == '[') {
            stack[++top] = *input;
        } else if (*input == '}') {
            if (top < 0 || stack[top] != '{') return -1;
            top--;
        } else if (*input == ']') {
            if (top < 0 || stack[top] != '[') return -1;
            top--;
        }
        input++;
    }
    return top == -1 ? 0 : -1;
}

int main(int argc, char **argv) {
    if (argc < 2) return 1;
    return parse_json(argv[1]);
}

Compile com AFL e execute o fuzzing.

5. Estratégias de Corpus e Dicionários

Criação de corpus mínimo

Para AFL, use afl-cmin para reduzir o corpus ao conjunto mínimo que cobre todos os caminhos:

afl-cmin -i input_corpus -o minimized_corpus -- ./parser_afl

Dicionários

Dicionários fornecem tokens específicos para guiar o fuzzer. Exemplo para JSON:

# json.dict
"null"
"true"
"false"
"["
"]"
"{"
"}"
":"
","

Use com AFL:

afl-fuzz -x json.dict -i input_corpus -o output_corpus -- ./parser_afl

Minimização com libFuzzer

Use -merge=1 para fundir corpus:

./fuzz_parser -merge=1 new_corpus existing_corpus

Reutilização de corpus

Corpus gerado por AFL pode ser usado no libFuzzer e vice-versa, pois ambos armazenam entradas binárias.

6. Integração com Sanitizers e Detecção de Bugs

AddressSanitizer (ASan)

Detecta buffer overflow, use-after-free, double free. Exemplo de crash:

==12345==ERROR: AddressSanitizer: heap-buffer-overflow on address 0x6020000000f0
READ of size 4 at 0x6020000000f0 thread T0
    #0 0x4a1b2c in process_data /src/fuzz_target.c:25
    #1 0x4a1d4e in LLVMFuzzerTestOneInput /src/fuzz_target.c:35

UndefinedBehaviorSanitizer (UBSan)

Detecta divisão por zero, overflow de inteiros, shift inválido.

LeakSanitizer (LSan)

Detecta vazamentos de memória. Ative com:

-fsanitize=address  # LSan é parte do ASan

Debugging com GDB

Para depurar um crash:

gdb --args ./fuzz_target crash_input
(gdb) run
(gdb) bt

7. Automação e Integração Contínua

Script de execução contínua

#!/bin/bash
# fuzz_runner.sh
while true; do
    afl-fuzz -i input_corpus -o output_corpus -t 1000 -- ./parser_afl
    sleep 10
done

CIFuzz com GitHub Actions

name: Fuzzing
on: [push, pull_request]
jobs:
  fuzz:
    runs-on: ubuntu-latest
    steps:
    - uses: actions/checkout@v3
    - name: Build and Fuzz
      run: |
        sudo apt install afl++
        afl-clang-fast -fsanitize=address -g -O2 fuzz_target.c -o fuzz_target
        afl-fuzz -i input_corpus -o output_corpus -t 500 -- ./fuzz_target

Cobertura com gcov e llvm-cov

clang -fprofile-instr-generate -fcoverage-mapping -fsanitize=fuzzer fuzz_target.c -o fuzz_target
LLVM_PROFILE_FILE="fuzz.profraw" ./fuzz_target
llvm-profdata merge -sparse fuzz.profraw -o fuzz.profdata
llvm-cov show ./fuzz_target -instr-profile=fuzz.profdata

8. Comparação e Boas Práticas

Quando usar cada ferramenta

Critério libFuzzer AFL
Velocidade Muito rápido (in-process) Rápido (forkserver)
Complexidade Simples, integrado ao Clang Requer compilação especial
Escalabilidade Melhor para funções isoladas Melhor para programas completos
CI Excelente Pode ser pesado
Corpus Gerenciamento automático Requer seeds iniciais

Limitações

  • Estado global: fuzzing de programas com estado global complexo é difícil.
  • Dependências de rede: simular entradas de rede requer mocking.
  • Sistemas de arquivos: fuzzing de operações de I/O exige wrappers.

Checklist de segurança

  • [ ] Sempre use -fsanitize=address,undefined
  • [ ] Defina timeouts: -max_total_time=3600 no libFuzzer
  • [ ] Limite memória: -rss_limit_mb=4096
  • [ ] Use dicionários para formatos conhecidos
  • [ ] Minimize corpus regularmente
  • [ ] Integre fuzzing ao CI

Próximos passos

Considere explorar Honggfuzz, que oferece fuzzing com hardware-based tracing e suporte a persistência, ideal para fuzzing de APIs de sistema.


Referências