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=3600no 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
- libFuzzer – A Library for Coverage-Guided Fuzz Testing — Documentação oficial do libFuzzer no LLVM, com exemplos e referência completa de flags.
- AFLplusplus/AFLplusplus — Repositório oficial do AFL++, com instruções de instalação, uso e contribuição.
- Fuzzing with AFL – Fuzzing Book — Capítulo do livro "The Fuzzing Book" que explica em detalhes o funcionamento do AFL.
- AddressSanitizer – Google — Página oficial do AddressSanitizer, com documentação sobre detecção de bugs de memória.
- CIFuzz – GitHub Actions for Fuzzing — Ferramenta da OSS-Fuzz para integrar fuzzing contínuo em repositórios via GitHub Actions.