Security/시스템 해킹(PWN, System)

Fuzzing101 with LibAFL - Part I: Fuzzing Xpdf

그믐​ 2023. 8. 18. 21:49
반응형

 

 

https://epi052.gitlab.io/notes-to-self/blog/2021-11-01-fuzzing-101-with-libafl/

 

Fuzzing101 with LibAFL - Part I: Fuzzing Xpdf -

Part one of a series covering fuzzer development using LibAFL

epi052.gitlab.io

위 블로그 글을 읽고 정리하였습니다.

블로그가 깔끔하고 탐나더군요..

 

블로그에서는 Part I 부터 시작해서 다양한 프로그램을 fuzzing하는데

fuzzing이 처음이라 Part 1을 정리하고자 합니다. 

 

Fuzzing101을 따라가며 공부하는 내용이고 AFL++에 중점을 두고 문제를 풀이했습니다.

 

환경 세팅은 블로그를 참조하도록 하고 fuzzer 사용부터 정리해보겠습니다.

 

Writing the fuzzer


fuzzer의 로직을 가지는 main.rs 부분을 작성합니다. LibAFL의 다양한 컴포넌트를 이용해서 만들 수 있습니다.

Corpus + Input

Corpus는 테스트 케이스의 집합을 나타내며, Input은 외부 소스에서 받은 데이터를 나타냅니다. 이 웹사이트에서는 InMemoryCorpus를 사용하여 테스트 케이스를 메모리에 저장하고, BytesInput을 사용하여 바이트 배열로 표현할 수 있는 데이터를 입력으로 사용합니다.

 

입력 corpus를 생성하고, 이를 InMemoryCorpus로 저장합니다. 그리고 BytesInput을 사용하여 입력 데이터를 처리합니다.

let corpus_dirs = vec![PathBuf::from("./corpus")];

let input_corpus = InMemoryCorpus::<BytesInput>::new();

그 다음으로 출력 corpus를 이동합니다. 입력 corpus에서 타임아웃을 유발하는 테스트 케이스는 "솔루션"으로 간주됩니다. 출력 corpus는 이러한 솔루션을 저장하는 corpus입니다.

let timeouts_corpus = OnDiskCorpus::new(PathBuf::from("./timeouts")).expect("Could not create timeouts corpus");

 

Observer

Observer는 현재 테스트 케이스에 대한 정보를 퍼저에 제공하는 역할을 합니다.
TimeObserver는 현재 테스트 케이스의 실행 시간을 추적합니다. 각 테스트 케이스에 대한 실행 시간을 Feedback 컴포넌트를 통해 퍼저에 전달합니다.
HitcountsMapObserver는 코드 커버리지를 추적합니다. 이를 위해 공유 메모리를 사용합니다. 공유 메모리 맵은 HitcountsMapObserver와 Executor 사이에서 공유됩니다.
공유 메모리 맵을 생성하려면 StdShMemProvider를 사용하여 새 공유 메모리 맵을 생성합니다. 이 맵은 65536 바이트입니다.
공유 메모리 ID를 환경에 저장하여 Executor가 알 수 있도록 합니다.
공유 메모리 맵에 대한 변경 가능한 참조를 가져옵니다. 이 참조는 &mut [u8] 유형입니다.
Observer를 생성하고 공유 메모리 맵 참조를 전달하며 이름을 "shared_mem"으로 지정합니다.
HitcountsMapObserver는 ConstMapObserver를 기반으로 합니다. ConstMapObserver는 MapObserver 위에 최적화 계층을 제공합니다. 컴파일 시에 알려진 맵 크기를 사용하여 테스트 케이스가 "흥미로운"지 여부를 결정할 때 성능 향상을 얻을 수 있습니다.
이 컴포넌트는 테스트 케이스의 실행 시간과 코드 커버리지를 추적하여 퍼저에 정보를 제공합니다. 이 정보는 퍼저가 테스트 케이스가 흥미로운지 여부를 결정하는 데 사용됩니다.

let time_observer = TimeObserver::new("time");
const MAP_SIZE: usize = 65536;
let mut shmem = StdShMemProvider::new().unwrap().new_map(MAP_SIZE).unwrap();
shmem.write_to_env("__AFL_SHM_ID").expect("couldn't write shared memory ID");
let mut shmem_map = shmem.as_mut_slice();
let edges_observer = unsafe { HitcountsMapObserver::new(StdMapObserver::new("shared_mem", shmem_buf)) };

 

Feedback

Feedback 컴포넌트는 테스트 케이스의 결과가 흥미로운지 여부를 분류합니다. 흥미로운 테스트 케이스로 분류되면 해당 테스트 케이스의 입력이 Corpus에 추가됩니다.
퍼저에는 여러 Feedback 컴포넌트가 필요합니다.

첫 번째로, HitcountsMapObserver를 MaxMapFeedback에 전달합니다. MaxMapFeedback는 HitcountsMapObserver의 코드 커버리지 맵에서 현재 최대 값보다 큰 값이 있는지 확인합니다. 새로운 최대 값이 발견되면 입력이 흥미로운 것으로 간주됩니다.
두 번째로, TimeFeedback를 생성하고 이전에 본 TimeObserver와 연결합니다. TimeFeedback는 입력이 흥미로운지 여부를 결정하는 데 도움이 되지 않지만, 테스트 케이스의 실행 시간을 추적합니다. 이 Feedback은 다른 Feedback과 함께 사용되어야 합니다.
두 Feedback 컴포넌트를 feedback_or 매크로를 사용하여 하나의 CombinedFeedback로 결합합니다. 이 결합은 논리 OR로 연결됩니다.
feedback 변수는 현재 테스트 케이스의 입력이 코드 커버리지 맵에서 새로운 코드 경로를 트리거하면 Corpus에 해당 입력을 저장해야 한다고 말합니다.

let mut feedback = feedback_or!(
 MaxMapFeedback::new_tracking(&edges_observer, true, false),
 TimeFeedback::new_with_observer(&time_observer)
);

이 코드는 MaxMapFeedback와 TimeFeedback를 결합하여 하나의 Feedback 컴포넌트를 생성합니다. 이 Feedback 컴포넌트는 코드 커버리지와 실행 시간을 추적하여 테스트 케이스의 결과가 흥미로운지 여부를 분류합니다.

 

State 

 State 컴포넌트는 퍼징 프로세스의 상태를 관리합니다. 퍼징 프로세스 중에 발생하는 모든 데이터와 정보를 저장하고 관리하는 역할을 합니다. 이 컴포넌트는 퍼저가 실행되는 동안 발견된 새로운 입력값, 코드 커버리지 정보, 크래시 정보 등을 저장합니다. 이 정보는 퍼징 프로세스를 중단하고 다시 시작할 때 유용하게 사용됩니다.

State 컴포넌트는 다양한 하위 컴포넌트를 포함하고 있습니다. 이 하위 컴포넌트들은 State 컴포넌트가 퍼징 프로세스의 상태를 효과적으로 관리할 수 있도록 돕습니다. State 컴포넌트의 주요 하위 컴포넌트에는 Corpus, Feedback, Objective 등이 있습니다.

State 컴포넌트는 퍼징 프로세스의 중요한 정보를 저장하고 관리하는 역할을 하므로, 퍼징 프로세스의 효율성과 성공률에 큰 영향을 미칩니다. 따라서 State 컴포넌트의 구현과 관리는 퍼징 프로세스의 성공에 매우 중요합니다.

use libafl::state::{HasClientPerfStats, HasCorpus, HasMetadata, HasSolutions};
use libafl::state::StdState;
use libafl::stats::SimpleStats;
use libafl::corpus::{Corpus, InMemoryCorpus, OnDiskCorpus};
use libafl::feedbacks::{CrashFeedback, Feedback, MaxMapFeedback};
use libafl::inputs::{BytesInput, HasTargetBytes};
use libafl::bolts::tuples::Named;
use libafl::bolts::serdeany::SerdeAnyMap;

let feedback = MaxMapFeedback::with_observer(&mut observer);
let feedback_state = feedback.as_mut().state();
let mut state = StdState::new(SimpleStats::new(), feedback_state, &mut executor, &mut fuzzer, &mut observer);

StdState::new 함수를 사용하여 State 컴포넌트를 생성합니다. 이 함수는 통계, 피드백 상태, 실행기, 퍼저, 관찰자를 매개변수로 받습니다.
MaxMapFeedback 클래스를 사용하여 피드백 컴포넌트를 생성합니다. 이 클래스는 관찰자를 매개변수로 받습니다.
SimpleStats 클래스를 사용하여 통계 컴포넌트를 생성합니다.
이 코드는 퍼징 프로세스의 상태를 관리하는 데 필요한 기본 구성을 보여줍니다. State 컴포넌트는 퍼징 프로세스의 중요한 정보를 저장하고 관리하는 역할을 합니다.

Monitor 

Monitor 컴포넌트는 모든 클라이언트를 추적하고 그들이 보고한 정보를 어떻게 표시할지에 대한 방법을 제공합니다. 이 예제에서는 가장 간단한 Monitor 표현인 SimpleMonitor를 사용합니다. SimpleMonitor는 SimpleMonitor::display를 입력으로 사용하여 터미널에 보고서를 보내기 위해 println을 호출합니다.

let monitor = SimpleMonitor::new(|s| println!("{s}"));

이 코드는 libAFL 라이브러리를 사용하여 퍼징 프로세스의 모니터링을 설정하는 예제입니다. 코드에서는 SimpleMonitor 클래스를 사용하여 모니터 컴포넌트를 생성하고 있습니다. SimpleMonitor는 터미널에 보고서를 출력하는 데 사용됩니다. 이 코드를 사용하여 퍼징 프로세스의 모니터링을 설정할 수 있습니다.

 

EventManager

EventManager 컴포넌트는 퍼징 루프 동안 생성되는 다양한 이벤트를 처리합니다. 이벤트의 예로는 흥미로운 테스트 케이스 찾기, Monitor 컴포넌트 업데이트, 로깅 등이 있습니다. 이 예제에서는 가장 간단한 유형의 EventManager인 SimpleEventManager를 사용합니다.

let mut mgr = SimpleEventManager::new(stats);

 

Scheduler 

Scheduler 컴포넌트는 퍼징 루프 동안 새로운 테스트 케이스를 입력 코퍼스에서 얻는 전략을 정의합니다. 이 예제에서는 IndexesLenTimeMinimizerScheduler를 사용합니다. 이 스케줄러는 큐를 기반으로 하며, 코퍼스에서 테스트 케이스를 얻는 데 사용됩니다. 이 스케줄러는 빠르고 작은 테스트 케이스를 우선시하며, 커버리지 맵의 메타데이터에 등록된 모든 항목을 실행합니다.

let scheduler = IndexesLenTimeMinimizerScheduler::new(QueueScheduler::new());

코드에서는 IndexesLenTimeMinimizerScheduler 클래스를 사용하여 스케줄러 컴포넌트를 생성하고 있습니다

 

Fuzzer 

 Fuzzer 컴포넌트는 피드백, 목표, 그리고 코퍼스 스케줄러를 포함하며, 프로그램을 실행하면서 생성된 입력을 사용하여 타겟 프로그램을 실행하고, Observer와 Feedback을 트리거합니다. 이 예제에서는 StdFuzzer를 사용합니다.

let mut fuzzer = StdFuzzer::new(scheduler, feedback, objective);

 

Executor 

Executor 컴포넌트는 타겟 프로그램을 실행하고, Observer를 트리거하여 테스트 케이스의 실행을 관찰합니다. 이 예제에서는 TimeoutForkserverExecutor를 사용합니다. TimeoutForkserverExecutor는 표준 ForkserverExecutor를 감싸고 각 실행 전에 타임아웃을 설정합니다. 이를 통해 자식 프로세스를 생성하여 퍼징을 수행하는 AFL과 유사한 메커니즘을 구현합니다.

let fork_server = ForkserverExecutor::builder()
 .program("./xpdf/install/bin/pdftotext")
 .parse_afl_cmdline(["@@"])
 .coverage_map_size(MAP_SIZE)
 .build(tuple_list!(time_observer, edges_observer))?;
let timeout = Duration::from_secs(5);
let mut executor = TimeoutForkserverExecutor::new(fork_server, timeout).unwrap();

코드에서는 TimeoutForkserverExecutor 클래스를 사용하여 실행자 컴포넌트를 생성하고 있습니다.

 

Mutator + Stage

Mutator와 Stage 컴포넌트는 퍼징 실행 중에 입력을 변형하는 데 사용됩니다. 이 예제에서는 StdScheduledMutator를 사용하여 Havoc 변형을 적용하고, 이를 StdMutationalStage 컴포넌트를 사용하여 단계로 등록합니다. 변형 단계는 퍼징 실행 중에 입력을 변형하는 단계입니다. 변형 단계는 일반적으로 한 번에 하나씩 적용되는 일련의 변형을 가지고 있습니다. 이 예제에서는 Havoc 변형을 사용하여 입력을 변형합니다.

let mutator = StdScheduledMutator::new(havoc_mutations());
let mut stages = tuple_list!(StdMutationalStage::new(mutator));

 

Running the Fuzzer


모든 개별 컴포넌트들을 결합하여 퍼저를 실행합니다.

퍼저는 피드백, 목표, 코퍼스 스케줄러를 포함하며, 프로그램을 실행하면서 생성된 입력을 사용하여 타겟 프로그램을 실행하고, Observer와 Feedback을 트리거합니다. 이 예제에서는 StdFuzzer를 사용하여 퍼징 루프를 실행합니다.

fuzzer
 .fuzz_loop(&mut stages, &mut executor, &mut state, &mut mgr)
 .expect("Error in the fuzzing loop");

 

빌드와 실행단계는 단순 명령어이므로 생략하겠습니다.

 

fuzzer를 구성하는 요소들에 대해서 공부할 수 있었습니다.

반응형