LibAFL
LibAFL은 AFL 기반 퍼저(예: AFL++)의 기능을 구현한 모듈식 퍼징 라이브러리입니다. 기존 퍼저와 달리 LibAFL은 모든 기능을 모듈식으로 제공하며, Rust 라이브러리로 자유롭게 커스터마이징할 수 있습니다. libFuzzer의 드롭인 대체제로 사용하거나, 처음부터 맞춤형 퍼저를 구축하는 라이브러리로 사용할 수 있습니다.
사용 시기
| 퍼저 | 최적 사용처 | 복잡도 |
|---|---|---|
| libFuzzer | 빠른 설정, 단일 스레드 | 낮음 |
| AFL++ | 다중 코어, 범용 | 중간 |
| LibAFL | 맞춤형 퍼저, 고급 기능, 연구 | 높음 |
다음과 같은 경우 LibAFL을 선택하세요:
- 맞춤형 변이 전략이나 피드백 메커니즘이 필요한 경우
- 표준 퍼저가 대상 아키텍처를 지원하지 않는 경우
- 새로운 퍼징 기법을 구현하려는 경우
- 퍼징 구성 요소를 세밀하게 제어해야 하는 경우
- 퍼징 연구를 수행하는 경우
빠른 시작
LibAFL은 최소한의 설정으로 libFuzzer의 드롭인 대체제로 사용할 수 있습니다:
extern "C" int LLVMFuzzerTestOneInput(const uint8_t *data, size_t size) {
// 퍼저가 제공하는 데이터로 코드 호출
my_function(data, size);
return 0;
}
LibAFL의 libFuzzer 호환성 레이어 빌드:
git clone https://github.com/AFLplusplus/LibAFL
cd LibAFL/libafl_libfuzzer_runtime
./build.sh
컴파일 및 실행:
clang++ -DNO_MAIN -g -O2 -fsanitize=fuzzer-no-link libFuzzer.a harness.cc main.cc -o fuzz
./fuzz corpus/
설치
전제 조건
- Clang/LLVM 15-18
- Rust (rustup 통해)
- 추가 시스템 종속성
Linux/macOS
Clang 설치:
apt install clang
또는 apt.llvm.org를 통해 특정 버전 설치:
wget https://apt.llvm.org/llvm.sh
chmod +x llvm.sh
sudo ./llvm.sh 15
Rust용 환경 변수 설정:
export RUSTFLAGS="-C linker=/usr/bin/clang-15"
export CC="clang-15"
export CXX="clang++-15"
Rust 설치:
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh
추가 종속성 설치:
apt install libssl-dev pkg-config
libFuzzer 호환성 모드의 경우, Nightly Rust 설치:
rustup toolchain install nightly --component llvm-tools
설치 확인
LibAFL을 빌드하여 설치 확인:
cd LibAFL/libafl_libfuzzer_runtime
./build.sh
# libFuzzer.a가 생성되어야 함
하네스 작성
LibAFL 하네스는 드롭인 대체 모드를 사용할 때 libFuzzer와 동일한 패턴을 따릅니다:
extern "C" int LLVMFuzzerTestOneInput(const uint8_t *data, size_t size) {
// 퍼징 대상 코드를 여기에 작성
return 0;
}
Rust 라이브러리로 맞춤형 퍼저를 구축할 때는 하네스 로직이 퍼저에 직접 통합됩니다. 전체 패턴은 아래 "맞춤형 퍼저 작성" 섹션을 참조하세요.
참고: 자세한 하네스 작성 기술은 하네스 작성 기술 스킬을 참조하세요.
사용 모드
LibAFL은 두 가지 주요 사용 모드를 지원합니다:
1. libFuzzer 드롭인 대체
기존 하네스와 함께 LibAFL을 libFuzzer 대체제로 사용합니다.
컴파일:
clang++ -DNO_MAIN -g -O2 -fsanitize=fuzzer-no-link libFuzzer.a harness.cc main.cc -o fuzz
실행:
./fuzz corpus/
장기 캠페인에 권장:
./fuzz -fork=1 -ignore_crashes=1 corpus/
2. Rust 라이브러리로 맞춤형 퍼저
LibAFL 구성 요소를 사용하여 완전히 맞춤화된 퍼저를 구축합니다.
프로젝트 생성:
cargo init --lib my_fuzzer
cd my_fuzzer
cargo add [email protected] [email protected] [email protected] [email protected] \
--features "[email protected]/libfuzzer,[email protected]/sancov_pcguard_hitcounts"
Cargo.toml 구성:
[lib]
crate-type = ["staticlib"]
맞춤형 퍼저 작성
참고: 자세한 하네스 작성 기술, 복잡한 입력 처리 패턴 및 고급 전략은 퍼즈-하네스-작성 기술 스킬을 참조하세요.
퍼저 구성 요소
LibAFL 퍼저는 모듈식 구성 요소로 구성됩니다:
- 관찰자 - 실행 피드백 수집 (커버리지, 타이밍)
- 피드백 - 입력이 흥미로운지 판단
- 목표 - 퍼징 목표 정의 (충돌, 타임아웃)
- 상태 - 코퍼스 및 메타데이터 유지
- 변이기 - 새로운 입력 생성
- 스케줄러 - 변이할 입력 선택
- 실행기 - 입력으로 대상 실행
기본 퍼저 구조
use libafl::prelude::*;
use libafl_bolts::prelude::*;
use libafl_targets::{libfuzzer_test_one_input, std_edges_map_observer};
#[no_mangle]
pub extern "C" fn libafl_main() {
let mut run_client = |state: Option<_>, mut restarting_mgr, _core_id| {
// 1. 관찰자 설정
let edges_observer = HitcountsMapObserver::new(
unsafe { std_edges_map_observer("edges") }
).track_indices();
let time_observer = TimeObserver::new("time");
// 2. 피드백 정의
let mut feedback = feedback_or!(
MaxMapFeedback::new(&edges_observer),
TimeFeedback::new(&time_observer)
);
// 3. 목표 정의
let mut objective = feedback_or_fast!(
CrashFeedback::new(),
TimeoutFeedback::new()
);
// 4. 상태 생성 또는 복원
let mut state = state.unwrap_or_else(|| {
StdState::new(
StdRand::new(),
InMemoryCorpus::new(),
OnDiskCorpus::new(&output_dir).unwrap(),
&mut feedback,
&mut objective,
).unwrap()
});
// 5. 변이기 설정
let mutator = StdScheduledMutator::new(havoc_mutations());
let mut stages = tuple_list!(StdMutationalStage::new(mutator));
// 6. 스케줄러 설정
let scheduler = IndexesLenTimeMinimizerScheduler::new(
&edges_observer,
QueueScheduler::new()
);
// 7. 퍼저 생성
let mut fuzzer = StdFuzzer::new(scheduler, feedback, objective);
// 8. 하네스 정의
let mut harness = |input: &BytesInput| {
let buf = input.target_bytes().as_slice();
libfuzzer_test_one_input(buf);
ExitKind::Ok
};
// 9. 실행기 설정
let mut executor = InProcessExecutor::with_timeout(
&mut harness,
tuple_list!(edges_observer, time_observer),
&mut fuzzer,
&mut state,
&mut restarting_mgr,
timeout,
)?;
// 10. 초기 입력 로드
if state.must_load_initial_inputs() {
state.load_initial_inputs(
&mut fuzzer,
&mut executor,
&mut restarting_mgr,
&input_dir
)?;
}
// 11. 퍼징 시작
fuzzer.fuzz_loop(&mut stages, &mut executor, &mut state, &mut restarting_mgr)?;
Ok(())
};
// 퍼저 실행
Launcher::builder()
.run_client(&mut run_client)
.cores(&cores)
.build()
.launch()
.unwrap();
}
컴파일
상세 모드
모든 계측 플래그를 수동으로 지정:
clang++-15 -DNO_MAIN -g -O2 \
-fsanitize-coverage=trace-pc-guard \
-fsanitize=address \
-Wl,--whole-archive target/release/libmy_fuzzer.a -Wl,--no-whole-archive \
main.cc harness.cc -o fuzz
컴파일러 래퍼 (권장)
LibAFL 컴파일러 래퍼를 생성하여 계측을 자동으로 처리합니다.
src/bin/libafl_cc.rs 생성:
use libafl_cc::{ClangWrapper, CompilerWrapper, Configuration, ToolWrapper};
pub fn main() {
let args: Vec<String> = env::args().collect();
let mut cc = ClangWrapper::new();
cc.cpp(is_cpp)
.parse_args(&args)
.link_staticlib(&dir, "my_fuzzer")
.add_args(&Configuration::GenerateCoverageMap.to_flags().unwrap())
.add_args(&Configuration::AddressSanitizer.to_flags().unwrap())
.run()
.unwrap();
}
컴파일 및 사용:
cargo build --release
target/release/libafl_cxx -DNO_MAIN -g -O2 main.cc harness.cc -o fuzz
참고: 자세한 sanitizer 구성, 일반적인 문제 및 고급 플래그는 주소-새니타이저 및 정의-되지-않음-동작-새니타이저 기술 스킬을 참조하세요.
캠페인 실행
기본 실행
./fuzz --cores 0 --input corpus/
다중 코어 퍼징
./fuzz --cores 0,8-15 --input corpus/
9개의 클라이언트 실행: 코어 0에서 하나, 코어 8-15에서 8개.
옵션과 함께 실행
./fuzz --cores 0-7 --input corpus/ --output crashes/ --timeout 1000
텍스트 사용자 인터페이스 (TUI)
그래픽 통계 보기 활성화:
./fuzz -tui=1 corpus/
출력 해석
| 출력 | 의미 |
|---|---|
corpus: N | 발견된 흥미로운 테스트 케이스 수 |
objectives: N | 발견된 충돌/타임아웃 수 |
executions: N | 총 대상 호출 수 |
exec/sec: N | 현재 실행 처리량 |
edges: X% | 코드 커버리지 비율 |
clients: N | 병렬 퍼징 프로세스 수 |
퍼저는 두 가지 주요 이벤트 유형을 방출합니다:
- UserStats - 현재 통계가 포함된 정기 하트비트
- Testcase - 새로 발견된 흥미로운 입력
고급 사용법
팁과 요령
| 팁 | 도움이 되는 이유 |
|---|---|
-fork=1 -ignore_crashes=1 사용 | 첫 충돌 후에도 퍼징 계속 |
InMemoryOnDiskCorpus 사용 | 재시작 간 코퍼스 유지 |
-tui=1로 TUI 활성화 | 진행 상황 시각화 향상 |
| 특정 LLVM 버전 사용 | 호환성 문제 방지 |
RUSTFLAGS 올바르게 설정 | 링킹 오류 방지 |
충돌 중복 제거
동일한 버그에서 발생한 중복 충돌 저장 방지:
백트레이스 관찰자 추가:
let backtrace_observer = BacktraceObserver::owned(
"BacktraceObserver",
libafl::observers::HarnessType::InProcess
);
실행기 업데이트:
let mut executor = InProcessExecutor::with_timeout(
&mut harness,
tuple_list!(edges_observer, time_observer, backtrace_observer),
&mut fuzzer,
&mut state,
&mut restarting_mgr,
timeout,
)?;
해시 피드백으로 목표 업데이트:
let mut objective = feedback_and!(
feedback_or_fast!(CrashFeedback::new(), TimeoutFeedback::new()),
NewHashFeedback::new(&backtrace_observer)
);
이렇게 하면 고유한 백트레이스가 있는 충돌만 저장됩니다.
사전 퍼징
사전을 사용하여 퍼징을 특정 토큰으로 유도:
파일에서 토큰 추가:
let mut tokens = Tokens::new();
if let Some(tokenfile) = &tokenfile {
tokens.add_from_file(tokenfile)?;
}
state.add_metadata(tokens);
변이기 업데이트:
let mutator = StdScheduledMutator::new(
havoc_mutations().merge(tokens_mutations())
);
하드코딩된 토큰 예제 (PNG):
state.add_metadata(Tokens::from([
vec![137, 80, 78, 71, 13, 10, 26, 10], // PNG 헤더
"IHDR".as_bytes().to_vec(),
"IDAT".as_bytes().to_vec(),
"PLTE".as_bytes().to_vec(),
"IEND".as_bytes().to_vec(),
]));
참고: 자세한 사전 생성 전략 및 형식별 사전은 퍼징-사전 기술 스킬을 참조하세요.
자동 토큰
프로그램에서 매직 값과 체크섬을 자동으로 추출:
컴파일러 래퍼에서 활성화:
cc.add_pass(LLVMPasses::AutoTokens)
퍼저에서 자동 토큰 로드:
tokens += libafl_targets::autotokens()?;
토큰 섹션 확인:
echo "p (uint8_t *)__token_start" | gdb fuzz
성능 튜닝
| 설정 | 영향 |
|---|---|
| 다중 코어 퍼징 | 코어 수에 따른 선형 속도 향상 |
InMemoryCorpus | 빠르지만 비영속적 |
InMemoryOnDiskCorpus | 속도와 영속성의 균형 |
| 새니타이저 | 2-5배 속도 저하, 버그 탐지에 필수 |
최적화 수준 -O2 | 속도와 커버리지 간 균형 |
퍼저 디버깅
쉬운 디버깅을 위해 단일 프로세스 모드로 퍼저 실행:
// 런처를 직접 호출로 대체
run_client(None, SimpleEventManager::new(monitor), 0).unwrap();
// 다음을 주석 처리:
// Launcher::builder()
// .run_client(&mut run_client)
// ...
// .launch()
그런 다음 GDB로 디버그:
gdb --args ./fuzz --cores 0 --input corpus/
실전 예제
예제: libpng
LibAFL을 사용한 libpng 퍼징:
1. 소스 코드 가져오기:
curl -L -O https://downloads.sourceforge.net/project/libpng/libpng16/1.6.37/libpng-1.6.37.tar.xz
tar xf libpng-1.6.37.tar.xz
cd libpng-1.6.37/
apt install zlib1g-dev
2. 컴파일러 래퍼 설정:
export FUZZER_CARGO_DIR="/path/to/libafl/project"
export CC=$FUZZER_CARGO_DIR/target/release/libafl_cc
export CXX=$FUZZER_CARGO_DIR/target/release/libafl_cxx
3. 정적 라이브러리 빌드:
./configure --enable-shared=no
make
4. 하네스 가져오기:
curl -O https://raw.githubusercontent.com/glennrp/libpng/f8e5fa92b0e37ab597616f554bee254157998227/contrib/oss-fuzz/libpng_read_fuzzer.cc
5. 퍼저 연결:
$CXX libpng_read_fuzzer.cc .libs/libpng16.a -lz -o fuzz
6. 시드 준비:
mkdir seeds/
curl -o seeds/input.png https://raw.githubusercontent.com/glennrp/libpng/acfd50ae0ba3198ad734e5d4dec2b05341e50924/contrib/pngsuite/iftp1n3p08.png
7. 사전 가져오기 (선택 사항):
curl -O https://raw.githubusercontent.com/glennrp/libpng/2fff013a6935967960a5ae626fc21432807933dd/contrib/oss-fuzz/png.dict
8. 퍼징 시작:
./fuzz --input seeds/ --cores 0 -x png.dict
예제: CMake 프로젝트
CMake 빌드 시스템과 LibAFL 통합:
CMakeLists.txt:
project(BuggyProgram)
cmake_minimum_required(VERSION 3.0)
add_executable(buggy_program main.cc)
add_executable(fuzz main.cc harness.cc)
target_compile_definitions(fuzz PRIVATE NO_MAIN=1)
target_compile_options(fuzz PRIVATE -g -O2)
계측되지 않은 바이너리 빌드:
cmake -DCMAKE_C_COMPILER=clang -DCMAKE_CXX_COMPILER=clang++ .
cmake --build . --target buggy_program
퍼저 빌드:
export FUZZER_CARGO_DIR="/path/to/libafl/project"
cmake -DCMAKE_C_COMPILER=$FUZZER_CARGO_DIR/target/release/libafl_cc \
-DCMAKE_CXX_COMPILER=$FUZZER_CARGO_DIR/target/release/libafl_cxx .
cmake --build . --target fuzz
퍼징 실행:
./fuzz --cores 0 --input seeds/
문제 해결
일반적인 문제
| 문제 | 해결책 |
|---|---|
| 링커 오류 | RUSTFLAGS에 올바른 Clang 버전이 설정되어 있는지 확인 |
LLVMFuzzerTestOneInput 다시 정의됨 | -DNO_MAIN 플래그로 컴파일 |
| 코어 덤프 | ASan 기호화를 위해 llvm-symbolizer 설치 |
| 느린 시작 | 시드 수 줄이기 (MaximumSizeStage 사용) |
| 높은 메모리 사용 | RSSLimitFeedback으로 메모리 제한 설정 |