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
을 입력하여 실행을 시작합니다.
환경 변수의 의미
AFL_NO_AFFINITY=1
을 하지 않을 시, 가끔 setaffinity 관련 에러가 발생합니다.unset ASAN_OPTIONS
를 하지 않을 시, 아래 에러가 발생합니다.
[-] 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_cursor2
는 0x4f83
이라는 유효하지 않은 주소를 참조하려다 오류가 발생한 것을 명확히 확인할 수 있습니다.
부록: GDB의 <optimized out>
디버깅 중 변수를 출력했을 때 아래와 같은 메시지를 마주칠 수 있습니다.
(gdb) p some_bitmap
$4 = <optimized out>
이는 컴파일러가 코드를 최적화하는 과정에서 해당 변수가 특정 시점 이후로 사용되지 않는다고 판단하여, 메모리나 레지스터 할당을 제거해버렸다는 의미입니다. 즉, “최적화되어 사라졌음"을 뜻합니다. 버그 추적에 꼭 필요한 변수가 이렇게 나온다면, -O0
와 같이 최적화 옵션을 끄고 다시 컴파일하여 디버깅을 진행해야 합니다.