AFL++
AFL++는 원본 AFL 퍼저의 포크로, 더 나은 퍼징 성능과 고급 기능을 제공하면서도 안정성을 유지합니다. libFuzzer에 비해 주요 이점은 다중 코어에서 퍼징 캠페인을 실행할 수 있는 안정적인 지원으로, 대규모 퍼징 작업에 이상적입니다.
사용 시기
| 퍼저 | 최적 대상 | 복잡도 |
|---|---|---|
| AFL++ | 다중 코어 퍼징, 다양한 변형, 성숙한 프로젝트 | 중간 |
| libFuzzer | 빠른 설정, 단일 스레드, 간단한 하니스 | 낮음 |
| LibAFL | 사용자 정의 퍼저, 연구, 고급 사용 사례 | 높음 |
AFL++를 선택해야 하는 경우:
- 처리량 극대화를 위해 다중 코어 퍼징이 필요한 경우
- 프로젝트를 Clang 또는 GCC로 컴파일할 수 있는 경우
- 다양한 변형 전략과 성숙한 도구를 원하는 경우
- libFuzzer가 한계에 도달하여 더 많은 커버리지가 필요한 경우
- 병렬 실행의 이점을 활용할 수 있는 프로덕션 코드베이스를 퍼징하는 경우
빠른 시작
extern "C" int LLVMFuzzerTestOneInput(const uint8_t *data, size_t size) {
// 퍼저 제공 데이터로 코드 호출
check_buf((char*)data, size);
return 0;
}
컴파일 및 실행:
# 먼저 AFL++ 래퍼 스크립트 설정 (설치 참조)
./afl++ docker afl-clang-fast++ -DNO_MAIN=1 -O2 -fsanitize=fuzzer harness.cc main.cc -o fuzz
mkdir seeds && echo "aaaa" > seeds/minimal_seed
./afl++ docker afl-fuzz -i seeds -o out -- ./fuzz
설치
AFL++는 LLVM, Python, Rust를 포함한 많은 종속성이 있습니다. AFL++ 퍼징에는 최신 Debian 또는 Ubuntu 배포판을 사용하는 것이 좋습니다.
| 방법 | 사용 시기 | 지원되는 컴파일러 |
|---|---|---|
| Ubuntu/Debian 저장소 | 최근 Ubuntu, 기본 기능만 | Ubuntu 23.10: Clang 14 및 GCC 13Debian 12: Clang 14 및 GCC 12 |
| Docker (Docker Hub에서) | 특정 AFL++ 버전, Apple Silicon 지원 | 4.35c 기준: Clang 19 및 GCC 11 |
| Docker (소스에서) | 출시되지 않은 기능 테스트, 패치 적용 | Dockerfile에서 구성 가능 |
| 소스에서 | Docker 회피, 특정 패치 필요 | LLVM_CONFIG 환경 변수로 조정 가능 |
Ubuntu/Debian
afl++를 설치하기 전에 apt-cache show afl++로 패키지의 clang 버전 종속성을 확인하고, 해당하는 lld 버전(예: lld-17)을 설치하십시오.
apt install afl++ lld-17
Docker (Docker Hub에서)
docker pull aflplusplus/aflplusplus:stable
Docker (소스에서)
git clone --depth 1 --branch stable https://github.com/AFLplusplus/AFLplusplus
cd AFLplusplus
docker build -t aflplusplus .
소스에서
Ubuntu 버전 요구 사항과 종속성은 Dockerfile을 참조하십시오. Clang 버전을 지정하려면 LLVM_CONFIG를 설정하십시오(예: llvm-config-18).
래퍼 스크립트 설정
호스트 또는 Docker에서 AFL++를 실행하기 위한 래퍼 스크립트를 만듭니다:
cat <<'EOF' > ./afl++
#!/bin/sh
AFL_VERSION="${AFL_VERSION:-"stable"}"
case "$1" in
host)
shift
bash -c "$*"
;;
docker)
shift
/usr/bin/env docker run -i \
--privileged \
-v ./:/src \
--rm \
--name "afl_fuzzing_$$" \
"aflplusplus/aflplusplus:$AFL_VERSION" \
bash -c "cd /src && bash -c \"$*\""
;;
*)
echo "사용법: $0 {host|docker}"
exit 1
;;
esac
EOF
chmod +x ./afl++
아래 예제는 시스템 구성 명령(호스트 커널에 도달해야 함)을 제외하고 docker 모드를 사용합니다. host를 대체하여 시스템에 직접 설치된 AFL++에 대해 실행할 수 있습니다. 래퍼는 모드 인수 이후의 모든 항목을 단일 셸 문자열로 결합하므로 따옴표는 유지되지 않습니다: 공백이 포함된 인수(-x "my dict.dict")는 단어 단위로 분할됩니다. 이러한 파일의 이름을 공백 없이 바꾸거나, 해당 실행에 대해 래퍼를 편집하십시오.
-t가 누락된 것은 의도적입니다. docker run -ti는 stdin이 터미널이 아닐 때마다 the input device is not a TTY 오류와 함께 중단되며, 이는 CI 작업 및 에이전트나 스크립트가 구동하는 모든 것을 포함합니다. afl-fuzz는 터미널이 없음을 감지하고 전체 화면 UI 대신 일반 상태 줄을 인쇄합니다. 터미널을 필요로 하는 프로그램(예: watch)은 래퍼의 호스트 쪽에서 실행해야 합니다. $$는 래퍼의 PID로 확장되므로 병렬 인스턴스는 단일 afl_fuzzing 이름에서 충돌하지 않고 고유한 컨테이너 이름을 얻습니다. docker ps는 COMMAND 열을 자르므로 모든 행이 비슷해 보입니다. 하나를 중지하기 전에 인스턴스를 구별하려면 docker ps --no-trunc를 사용하십시오.
보안 경고: afl-system-config 및 afl-persistent-config 스크립트는 루트 권한이 필요하며 OS 보안 기능을 비활성화합니다. 프로덕션 시스템이나 개발 환경에서 퍼징하지 마십시오. 전용 VM을 사용하십시오.
시스템 구성
재부팅 후 초당 최대 15% 더 많은 실행을 위해 실행:
./afl++ host afl-system-config
afl-system-config는 실행되는 커널을 조정하므로 캠페인을 호스팅하는 시스템에서 실행하십시오. ./afl++ docker afl-system-config는 래퍼의 --privileged 컨테이너를 통해 동일한 설정에 도달하며, 이는 Docker를 통해서만 AFL++가 설치된 경우 유일한 방법입니다.
최대 성능을 위해 커널 보안 완화를 비활성화합니다(grub 부트로더 필요, Docker에서 지원되지 않음):
./afl++ host afl-persistent-config
update-grub
reboot
./afl++ host afl-system-config
cat /proc/cmdline으로 확인 - 출력에 mitigations=off가 포함되어야 합니다.
하니스 작성
하니스 구조
AFL++는 libFuzzer 스타일 하니스를 지원합니다:
#include <stdint.h>
#include <stddef.h>
extern "C" int LLVMFuzzerTestOneInput(const uint8_t *data, size_t size) {
// 1. 필요한 경우 입력 크기 검증
if (size < MIN_SIZE || size > MAX_SIZE) return 0;
// 2. 퍼즈 데이터로 대상 함수 호출
target_function(data, size);
// 3. 0 반환 (0이 아닌 값은 향후 사용을 위해 예약됨)
return 0;
}
하니스 규칙
| 실행할 사항 | 실행하지 말아야 할 사항 |
|---|---|
| 실행 간 전역 상태 재설정 | 이전 실행의 상태에 의존 |
| 엣지 케이스를 적절히 처리 | 잘못된 입력에서 종료 |
| 하니스를 결정론적으로 유지 | 난수 생성기 사용 |
| 할당된 메모리 해제 | 메모리 누수 생성 |
| 입력 크기 검증 | 무제한 입력 처리 |
참고: 자세한 하니스 작성 기술, 복잡한 입력 처리를 위한 패턴 및 고급 전략은 fuzz-harness-writing 기술 스킬을 참조하십시오.
컴파일
AFL++는 다양한 컴파일 모드를 제공하며, 각각 장단점이 있습니다.
컴파일 모드 결정 트리
컴파일 모드를 선택하십시오:
- LTO 모드 (
afl-clang-lto): 최고의 성능 및 계측. 먼저 시도하십시오. - LLVM 모드 (
afl-clang-fast): LTO가 컴파일 실패 시 대체. - GCC 플러그인 (
afl-gcc-fast): GCC가 필요한 프로젝트용.
기본 컴파일 (LLVM 모드)
./afl++ docker afl-clang-fast++ -DNO_MAIN=1 -O2 -fsanitize=fuzzer harness.cc main.cc -o fuzz
GCC 컴파일
./afl++ docker afl-g++-fast -DNO_MAIN=1 -O2 -fsanitize=fuzzer harness.cc main.cc -o fuzz
중요: GCC 버전은 AFL++ GCC 플러그인을 컴파일하는 데 사용된 버전과 일치해야 합니다.
샘플라이저와 함께
./afl++ docker AFL_USE_ASAN=1 afl-clang-fast++ -DNO_MAIN=1 -O2 -fsanitize=fuzzer harness.cc main.cc -o fuzz
참고: 자세한 샘플라이저 구성, 일반적인 문제 및 고급 플래그는 address-sanitizer 및 undefined-behavior-sanitizer 기술 스킬을 참조하십시오.
빌드 플래그
-g는 필요하지 않습니다. AFL++ 컴파일러에 의해 기본적으로 추가됩니다.
| 플래그 | 목적 |
|---|---|
-DNO_MAIN=1 | libFuzzer 하니스 사용 시 main 함수 건너뛰기 |
-O2 | 프로덕션 최적화 수준 (퍼징에 권장) |
-fsanitize=fuzzer | libFuzzer 호환 모드 활성화 및 실행 파일 링크 시 퍼저 런타임 추가 |
-fsanitize=fuzzer-no-link | 퍼저 런타임 링크 없이 계측 (정적 라이브러리 및 오브젝트 파일용) |
코퍼스 관리
초기 코퍼스 생성
AFL++는 최소한 하나의 비어 있지 않은 시드 파일이 필요합니다:
mkdir seeds
echo "aaaa" > seeds/minimal_seed
실제 프로젝트의 경우, 대표적인 입력을 수집하십시오:
- 퍼징하려는 형식의 예제 파일 다운로드
- 프로젝트의 테스트 스위트에서 테스트 케이스 추출
- 파일 형식에 대한 최소 유효 입력 사용
코퍼스 최소화
캠페인 후, 고유한 커버리지만 유지하도록 코퍼스를 최소화합니다:
./afl++ docker afl-cmin -i out/default/queue -o minimized_corpus -- ./fuzz
참고: 코퍼스 생성 전략, 사전 및 시드 선택은 fuzzing-corpus 기술 스킬을 참조하십시오.
캠페인 실행
기본 실행
./afl++ docker afl-fuzz -i seeds -o out -- ./fuzz
환경 변수 설정
./afl++ docker AFL_FAST_CAL=1 afl-fuzz -i seeds -o out -- ./fuzz
출력 해석
AFL++는 두 방식 모두 통계를 보고하지만, 읽는 방법은 모드에 따라 다릅니다. 래퍼의 docker run -i는 컨테이너에 TTY를 제공하지 않으므로 afl-fuzz는 전체 화면 UI를 포기하고 일반 상태 줄을 로그에 씁니다 — 아래 필드는 거기와 state/<인스턴스>/fuzzer_stats에 나타납니다. 대화형 UI를 얻으려면 터미널에서 host 모드를 실행하거나, 수동으로 보고 있는 실행에 대해 래퍼에 -t를 추가하십시오.
| 출력 | 의미 |
|---|---|
| execs/sec | 실행 속도 - 높을수록 좋음 |
| cycles done | 완료된 큐 패스 수 |
| corpus count | 큐의 고유 테스트 케이스 수 |
| saved crashes | 발견된 고유 크래시 수 |
| stability | 안정적인 에지의 % (100%에 가까워야 함) |
출력 디렉토리 구조
out/default/
├── cmdline # SUT가 어떻게 호출되었는가?
├── crashes/ # SUT를 크래시시키는 입력
│ └── id:000000,sig:06,src:000002,time:286,execs:13105,op:havoc,rep:4
├── hangs/ # SUT를 중단시키는 입력
├── queue/ # 최종 퍼저 상태를 재현하는 테스트 케이스
│ ├── id:000000,time:0,execs:0,orig:minimal_seed
│ └── id:000001,src:000000,time:0,execs:8,op:havoc,rep:6,+cov
├── fuzzer_stats # 캠페인 통계
└── plot_data # 플롯 데이터
결과 분석
라이브 캠페인 통계 보기:
./afl++ docker afl-whatsup out
커버리지 플롯 생성. aflplusplus 이미지에는 이미 gnuplot-nox가 포함되어 있습니다. 호스트 모드에서는 먼저 apt install gnuplot로 gnuplot을 설치하십시오.
./afl++ docker afl-plot out/default out_graph/
테스트 케이스 재실행
out/default/crashes/의 파일 이름 중 하나를 전달하십시오:
./afl++ docker ./fuzz out/default/crashes/id:000000,sig:06,src:000002,time:286,execs:13105,op:havoc,rep:4
퍼저 옵션
| 옵션 | 목적 |
|---|---|
-G 4000 | 최대 테스트 입력 길이 (기본값: 1048576 바이트) |
-t 1000 | 각 테스트 케이스에 대한 시간 초과 (밀리초, 기본값: 1000ms) |
-m 1000 | 메모리 제한 (메가바이트, 기본값: 0 = 무제한) |
-x ./dict.dict | 변형을 안내하는 사전 파일 사용 |
중요한 환경 변수
AFL++에는 많은 환경 변수가 있지만, 대부분은 특수한 경우입니다. 실제로 중요한 것은 다음과 같습니다.
항상 설정하십시오
# 모든 캠페인은 tmpfs를 사용해야 합니다 — SSD가 감사할 것이며 더 빠릅니다
AFL_TMPDIR=/dev/shm
AFL_TMPDIR은 단점이 없는 무료 성능 향상입니다 — 설정하지 않으면 SSD를 닳게 하고 퍼징 속도를 늦춥니다.
느린 대상
# 캘리브레이션 속도 약 2.5배 향상 — 대상이 느린 경우 사용 (예: >10 ms/exec)
AFL_FAST_CAL=1
AFL_FAST_CAL은 무시할 수 있는 정밀도 손실로 캘리브레이션 시간을 줄입니다. 캘리브레이션이 오래 걸리는 느린 대상에 특히 권장됩니다.
다중 코어 캠페인
# 기본(-M) 인스턴스에서만 — afl-cmin에 필요, 퍼징 자체에는 필요 없음
AFL_FINAL_SYNC=1
# 모든 인스턴스에서 — 테스트 케이스를 메모리에 캐시 (기본값: 50 MB, 적정 범위: 50-250 MB)
AFL_TESTCACHE_SIZE=100
AFL_FINAL_SYNC는 기본 인스턴스가 중지 시 모든 보조 인스턴스에서 최종 가져오기를 수행하도록 지시합니다. 이는 퍼징 프로세스 자체에 영향을 미치지 않으며, 나중에 코퍼스 최소화를 위해 afl-cmin을 실행할 때만 중요하여 기본의 큐에 전체 결합 코퍼스가 있도록 보장합니다. AFL_TESTCACHE_SIZE는 테스트 케이스를 메모리에 캐시하여 디스크 I/O를 줄입니다. 기본값은 50 MB이며, 대부분의 캠페인에 50-250 MB 값이 잘 작동합니다.
CI/자동화된 퍼징
# 퍼징이 아무것도 찾지 못하면 빠르게 실패
AFL_EXIT_ON_TIME=3600 # 새 경로 없음 1시간 = 중지
# 또는 "완료"될 때까지 실행 (모든 큐 항목 처리)
AFL_EXIT_WHEN_DONE=1
# 헤드리스 환경
AFL_NO_UI=1
CI에서 무제한 퍼징은 리소스를 낭비합니다. 시간 제한을 설정하거나 종료 조건을 사용하십시오.
피해야 할 변수
| 변수 | 건너뛰는 이유 |
|---|---|
AFL_NO_ARITH | 바이너리 형식의 커버리지를 해칠 수 있으나 텍스트 기반 대상에는 유용할 수 있음 |
AFL_SHUFFLE_QUEUE | 특수한 설정에서만, 일반적으로 해로움 |
AFL_DISABLE_TRIM | 트리밍은 가치 있음, 이유 없이 비활성화하지 마십시오 |
다중 코어 퍼징
AFL++는 두 가지 주요 이점으로 다중 코어 퍼징에 탁월합니다:
- 초당 더 많은 실행 (물리적 코어에 선형적으로 확장)
- 비대칭 퍼징 (예: 하나의 ASan 작업, 나머지는 샘플라이저 없음)
캠페인 시작
기본 퍼저 시작 (백그라운드):
./afl++ docker afl-fuzz -M primary -i seeds -o state -- ./fuzz 1>primary.log 2>primary.error </dev/null &
보조 퍼저 시작 (코어 수만큼):
./afl++ docker afl-fuzz -S secondary01 -i seeds -o state -- ./fuzz 1>secondary01.log 2>secondary01.error </dev/null &
./afl++ docker afl-fuzz -S secondary02 -i seeds -o state -- ./fuzz 1>secondary02.log 2>secondary02.error </dev/null &
</dev/null은 장식이 아닌 필수입니다. docker run -i는 클라이언트가 자체 stdin을 읽도록 유지하며, 터미널을 읽는 백그라운드 프로세스는 SIGTTIN을 받습니다. 파이프를 less로 연결하는 경우에도 마찬가지입니다. </dev/null은 stdin을 닫아 이 문제를 방지합니다.
상태 읽기
모든 인스턴스에 대한 요약 보기:
./afl++ docker afl-whatsup state
보조 구성
보조 인스턴스는 다른 옵션을 사용할 수 있습니다:
# ASan을 사용한 하나의 보조 (느리지만 버그를 더 잘 찾음)
./afl++ docker AFL_USE_ASAN=1 afl-fuzz -S asan_secondary -i seeds -o state -- ./fuzz </dev/null &
# 제한된 메모리를 사용한 또 다른 보조
./afl++ docker afl-fuzz -S limited_mem_secondary -i seeds -o state -m 500 -- ./fuzz </dev/null &
지속 모드
지속 모드는 새 프로세스를 생성하지 않고 동일한 프로세스에서 여러 입력을 처리하여 퍼징 속도를 크게 높입니다. 이는 초기화 오버헤드가 높은 라이브러리에 유용합니다.
기본 구현
#include <stdint.h>
#include <stddef.h>
__AFL_FUZZ_INIT();
extern "C" int LLVMFuzzerTestOneInput(const uint8_t *data, size_t size) {
__AFL_INIT();
target_function(data, size);
return 0;
}
하니스 패턴 요약
| 패턴 | 단계 | 설명 |
|---|---|---|
| 기본 | 퍼저 시작 → LLVMFuzzerTestOneInput 호출 | 가장 간단한 패턴, 함수가 상태를 재설정하지 않는 경우 유용 |
| 사전 초기화 | 퍼저 시작 → 하니스가 전역 상태 초기화 → LLVMFuzzerTestOneInput 호출 | __AFL_INIT()는 초기 초기화 후 퍼저 상태를 저장하여 각 반복에서 해당 오버헤드를 건너뜀 |
참고: 자세한 지속 모드 패턴, 벤치마킹 및 문제 해결은 aflpp-persistent-mode 기술 스킬을 참조하십시오.
고급 주제
딕셔너리 기반 퍼징
./afl++ docker afl-fuzz -x spec.dict -i seeds -o out -- ./fuzz
표준 AFL++ 변형은 특수 토큰을 사용하는 형식(예: PDF, JPEG)용 딕셔너리에 도움이 됩니다. JSON 및 XML과 같은 텍스트 기반 형식의 경우 AFL++는 종종 이를 자동으로 학습합니다.
크래시 분류
크래시를 디버거로 재실행:
gdb --args ./fuzz out/default/crashes/<크래시 파일>
AFL++ 크래시를 독립 실행형 테스트로 변환:
#include <stdint.h>
#include <stddef.h>
#include <fstream>
int main(int argc, char** argv) {
std::ifstream file(argv[1], std::ios::binary);
std::vector<uint8_t> data((std::istreambuf_iterator<char>(file)), std::istreambuf_iterator<char>());
LLVMFuzzerTestOneInput(data.data(), data.size());
return 0;
}
문제 해결
| 문제 | 해결책 |
|---|---|
afl-fuzz: no instrumentation found | - afl-clang-fast/afl-gcc-fast로 컴파일되었는지 확인- 타겟 바이너리에서 nm 출력 확인: afl-qemu-trace를 찾을 수 있거나 계측 심볼이 표시되어야 함 |
| 세분화 오류 | - AFL_USE_ASAN=1로 다시 컴파일- 크래시를 디버거로 재실행 (gdb ./fuzz <testcase>) |
| 낮은 초당 실행 수 | - AFL_TMPDIR=/dev/shm 설정 확인- AFL_FAST_CAL=1 시도- CPU 코어가 충분한지 확인 |
| 퍼저 시작 실패 | - /proc/sys/kernel/core_pattern 확인 (적절히 설정: echo core > /proc/sys/kernel/core_pattern)- afl-system-config가 실행되었는지 확인 |
Hmm, your system is configured to send core dump notifications to an external utility... | 래퍼 스크립트로 Docker를 사용하는 경우, 호스트에서도 afl-system-config를 실행해야 하거나 Docker 컨테이너 내에서 수동으로 core_pattern을 설정해야 합니다: echo core > /proc/sys/kernel/core_pattern |
추가 리소스
관련 기술 스킬
이 스킬은 다음 기술과 함께 사용해야 합니다:
- fuzz-harness-writing — 하니스 패턴, 딕셔너리 지원, 문제 해결
- fuzzing-corpus — 효과적인 시드 생성 및 구성
- address-sanitizer — AddressSanitizer 구성 및 분석
- undefined-behavior-sanitizer — UndefinedBehaviorSanitizer 구성 및 분석
- aflpp-persistent-mode — AFL++ 지속 모드 패턴 및 벤치마킹