Park, Geon/(방법) AFL (AFL++) 소스코드 수정 시 디버깅 - gdb 사용

Created Mon, 06 Oct 2025 21:27:24 +0900 Modified Tue, 07 Oct 2025 12:39:12 +0900
2085 Words

AFL의 소스코드를 뜯어고치며 새로운 퍼징 도구를 만들어 퍼징을 하다 보면 타겟 프로그램이 아닌, afl-fuzz 퍼저 자체가 비정상적으로 종료될 때가 있습니다. 이럴 때 필요한 것이 바로 GDB를 이용한 디버깅입니다.

이 글에서는 GDB를 활용하여 afl-fuzz의 세그멘테이션 오류(Segmentation Fault)를 추적하고 원인을 찾아내는 과정을 단계별로 정리합니다.

1. GDB로 AFL/AFL++ 실행하기

디버깅의 첫걸음은 GDB 환경에서 퍼저를 실행하는 것입니다. 일반적인 실행 명령어 앞에 gdb --args를 붙여주면 간단하게 시작할 수 있습니다.

# 디버깅할 프로그램과 인자 설정
prog=swftophp-4.7
cmdline="@@"

# 이전 실행 결과 및 ASAN 옵션 초기화
rm -rf $output_dir
unset ASAN_OPTIONS

# 환경 변수 설정
export AFL_DEBUG=1        # AFL 디버깅 메시지 활성화 (디버깅 시 유용)
export AFL_NO_AFFINITY=1  # CPU 코어 고정 기능 비활성화 (디버깅 시 유용)

# GDB로 afl-fuzz 실행
gdb --args ./afl-fuzz -m none -d -i seed -o $output_dir \
	-- ./$prog \
	$cmdline

GDB 프롬프트가 뜨면, r 또는 run을 입력하여 실행을 시작합니다.

환경 변수의 의미

[-] PROGRAM ABORT : Custom ASAN_OPTIONS set without abort_on_error=1 - please fix!
         Location : check_asan_opts(), afl-fuzz.c:

GDB 기능 활용: 디버깅 자동화

GDB를 반복적으로 사용하다 보면, 특히 크래시가 발생했을 때 run, bt를 입력하는 과정이 번거롭게 느껴질 수 있습니다. GDB는 이러한 작업을 자동화할 수 있는 강력한 옵션을 제공합니다.

-ex 옵션을 이용한 실행 - 백트레이스 - 루프 자동실행

GDB의 -ex <명령어> 옵션을 사용하면 GDB가 시작되자마자 지정된 명령어를 실행하게 할 수 있습니다. 이 옵션을 여러 번 사용하여 명령어 체인을 만들면, 크래시 발생 시의 정보 수집을 자동화할 수 있습니다.

예를 들어 AFL++에서 크래시가 발생했을 때, 자동으로 프로그램을 실행(run)하고, 콜 스택을 확인(bt)한 뒤, GDB를 종료(quit)하는 명령어는 다음과 같습니다.

prog=swftophp-4.7
cmdline="@@"

# GDB를 통해 실행 후 백트레이스를 찍고 바로 종료하는 명령어
AFL_DEBUG=1 AFL_DEBUG_CHILD=1 gdb \
  -ex run \
  -ex bt \
  -ex quit \
  --args ./afl-fuzz -m none -i seed -o $output_dir \
  -- ./$prog \
  $cmdline

이 스크립트는 GDB가 실행되자마자 프로그램을 구동하고, 만약 크래시로 멈추면 그 즉시 콜 스택(bt)을 화면에 출력한 후 자동으로 종료됩니다.

--batch 모드를 활용한 완전 자동화 및 병렬 실행

여기서 한 걸음 더 나아가, GDB를 스크립트 내에서 완벽한 자동화 도구로 사용할 수 있습니다. --batch 옵션은 GDB를 대화형 프롬프트 없이 실행하고, -ex로 주어진 모든 명령어 수행 후 즉시 종료시키는 ‘배치 모드’로 작동하게 합니다.

이 스크립트를 활용하면 여러 퍼저 인스턴스를 동시에 실행하고, 각 인스턴스에서 발생하는 크래시 로그를 별도의 파일에 자동으로 저장하는 병렬 디버깅 환경을 구축할 수 있습니다.

GDB_LOG_FILE="/path/to/gdb_crash.log"
touch "$GDB_LOG_FILE"

echo "Starting fuzzer with GDB in batch mode..."

gdb --batch \
    -ex "run" \
    -ex "bt" \
    -ex "quit" \
    --args ./afl-fuzz -m none -i seed -o output \
    -- ./$prog \
    $cmdline \
    &> $GDB_LOG_FILE &

위 스크립트의 주요 특징은 다음과 같습니다.

  • gdb --batch: GDB를 스크립트 실행에 적합한 비대화형(non-interactive) 모드로 실행합니다.

  • &> $GDB_LOG_FILE: 표준 출력과 표준 오류를 모두 지정된 로그 파일로 리디렉션합니다. 크래시 발생 시 bt의 결과가 이 파일에 저장됩니다.

  • &: GDB 프로세스를 백그라운드에서 실행하여, 터미널을 계속 사용하거나 다른 GDB 인스턴스를 추가로 실행할 수 있게 합니다.

이처럼 -ex--batch 옵션을 조합하면 GDB를 단순한 디버거를 넘어, 강력한 자동화 크래시 분석 도구로 활용할 수 있습니다. 🚀

2. 디버깅 과정 따라가기: 오류 추적 실전

이제 실제 디버깅 과정을 따라가 보겠습니다.

1단계: 최초 실행 및 오류 확인

GDB에서 run 명령어로 퍼저를 실행하자마자 세그멘테이션 오류가 발생하며 프로그램이 멈췄습니다. GDB는 친절하게도 어느 부분에서 오류가 발생했는지 바로 알려줍니다.

if (*cursor & *cursor2) {

오류가 발생한 코드 라인을 확인했으니, 이제 원인을 파악할 차례입니다.

2단계: 브레이크포인트 설정

원인을 정확히 분석하기 위해 오류가 발생한 라인 바로 직전에 브레이크포인트(breakpoint) 를 설정합니다. 이렇게 하면 해당 코드 실행 직전에 프로그램을 멈추고 주변 변수들의 상태를 살펴볼 수 있습니다.

# GDB 프롬프트에서 아래와 같이 브레이크포인트 설정 (라인 번호는 예시)
(gdb) b afl-fuzz.c:3398

브레이크포인트를 설정한 뒤, 다시 r 명령어로 프로그램을 실행합니다.

3단계: 변수 값 확인 및 원인 분석

프로그램이 브레이크포인트에서 멈추면, p 또는 print 명령어로 문제의 원인으로 의심되는 변수들의 값을 확인합니다.

Breakpoint 1, upd_tgts () at afl-fuzz.c:3398
3398          for (u32 i = 0; i < SOMETHING_SIZE; i++) {

(gdb) p cursor
$1 = (u8 *) 0x5555556a0128 "\b"

(gdb) p cursor2
$2 = (u8 *) 0x4f83 <error: Cannot access memory at address 0x4f83>

여기에서 dfg_cursor는 유효한 메모리 주소를 가리키고 있지만, dfg_cursor20x4f83이라는 유효하지 않은 주소를 참조하려다 오류가 발생한 것을 명확히 확인할 수 있습니다.


부록: GDB의 <optimized out>

디버깅 중 변수를 출력했을 때 아래와 같은 메시지를 마주칠 수 있습니다.

(gdb) p some_bitmap
$4 = <optimized out>

이는 컴파일러가 코드를 최적화하는 과정에서 해당 변수가 특정 시점 이후로 사용되지 않는다고 판단하여, 메모리나 레지스터 할당을 제거해버렸다는 의미입니다. 즉, “최적화되어 사라졌음"을 뜻합니다. 버그 추적에 꼭 필요한 변수가 이렇게 나온다면, -O0와 같이 최적화 옵션을 끄고 다시 컴파일하여 디버깅을 진행해야 합니다.