
하드웨어 브레이크포인트를 활용한 임베디드 시스템 퍼징
이 저장소는 논문 'Fuzzing Embedded Systems using Debugger Interfaces'의 동반 코드입니다. 논문의 사전 인쇄본은 https://publications.cispa.saarland/3950/ 에서 찾을 수 있습니다. 이 코드는 사용자가 논문에 보고된 결과를 재현하고 확장할 수 있도록 합니다. 결과를 보고, 재현하거나 확장할 때는 위 논문을 인용해 주시기 바랍니다.
.
├── benchmark # Google의 퍼저 테스트 스위트를 빌드하고 실험을 실행하는 스크립트
├── dependencies # GDBFuzz의 종속성을 설치하는 Makefile 포함
├── evaluation # 논문에 제시된 원시 실험 데이터
├── example_firmware # 평가에 사용된 임베디드 예제 애플리케이션
├── example_programs # GDBFuzz를 테스트하기 위한 컴파일된 예제 프로그램 및 설정 포함
├── src # GDBFuzz 구현 포함
├── Dockerfile # 모든 GDBFuzz 종속성이 설치된 Docker 이미지 생성을 위한 파일
├── LICENSE # 라이선스
├── Makefile # 도커 이미지 생성 또는 GDBFuzz를 로컬에 설치하기 위한 Makefile
└── README.md # 이 README 파일
__GDBFuzz__의 아이디어는 마이크로컨트롤러의 하드웨어 중단점을 커버리지 기반 퍼징의 피드백으로 활용하는 것입니다. 이를 위해 GDB를 일반 인터페이스로 사용하여 광범위한 적용 가능성을 확보합니다. 펌웨어의 바이너리 분석에는 Ghidra가 사용됩니다. 이 코드는 방법을 평가하기 위한 벤치마크 설정을 포함합니다. 또한 예제 펌웨어 파일이 포함되어 있습니다.
GDBFuzz는 임베디드 시스템을 위한 커버리지 기반 퍼징을 가능하게 하지만, 평가 목적으로 임의의 사용자 애플리케이션을 퍼징할 수도 있습니다. 마이크로컨트롤러에서 퍼징하려면 GDBFuzz를 로컬에 설치하여 테스트 대상 장치에 퍼즈 데이터를 완벽하게 전송할 수 있도록 권장합니다.
__GDBFuzz__는 Ubuntu 20.04 LTS 및 Raspberry Pi OS 32-bit에서 테스트되었습니다. 필수 구성 요소는 Java와 python3입니다. 먼저 새로운 가상 환경을 만들고 모든 종속성을 설치합니다.
virtualenv .venv
source .venv/bin/activate
make
chmod a+x ./src/GDBFuzz/main.py
GDBFuzz는 다음 키가 포함된 설정 파일에서 설정을 읽습니다.
[SUT]
# SUT의 바이너리 파일 경로.
# 예를 들면 .elf 파일 또는 .bin 파일일 수 있습니다.
binary_file_path = <path>
# CFG의 루트 노드 주소.
# 중단점은 이 CFG의 노드에 배치됩니다.
# 예: 'LLVMFuzzerTestOneInput' 또는 'main'
entrypoint = <entrypoint>
# 중단점이 회전될 때까지 중단점이 적중되지 않고 실행되어야 하는 입력 수.
until_rotate_breakpoints = <number>
# 한 번에 배치할 수 있는 최대 중단점 수.
max_breakpoints = <number>
# 무시할 블랙리스트 함수.
# ignore_functions는 공백으로 구분된 함수 이름 목록입니다 (예: 'malloc free').
ignore_functions = <space separated list>
# {Hardware, QEMU, SUTRunsOnHost} 중 하나
# Hardware: 외부 구성 요소가 gdb 서버를 시작하고 GDBFuzz가 이 gdb 서버에 연결할 수 있습니다.
# QEMU: GDBFuzz가 QEMU를 시작합니다. QEMU는 binary_file_path를 에뮬레이트하고 gdbserver를 시작합니다.
# SUTRunsOnHost: GDBFuzz가 GDB 내에서 대상 프로그램을 시작합니다.
target_mode = <mode>
# Ghidra를 시작하고, SUT를 분석한 다음,
# Ghidra 브리지 서버를 수동으로 시작하려면 False로 설정하십시오.
start_ghidra = True
# 소프트웨어 중단점(오류 처리 코드용)이 설정된 주소의 공백으로 구분된 목록.
# 이 중단점의 실행은 크래시로 간주됩니다.
# 예: software_breakpoint_addresses = 0x123 0x432
software_breakpoint_addresses =
# 트리거된 모든 소프트웨어 중단점을 크래시로 간주할지 여부
consider_sw_breakpoint_as_error = False
[SUTConnection]
# 파일 'SUT_connection_path'의 클래스 'SUT_connection_class'는
# 입력을 SUT로 보내는 방법을 구현합니다.
# 입력은 Wi-Fi, 시리얼, 블루투스 등을 통해 전송될 수 있습니다.
# 이 클래스는 ./connections/SUTConnection.py를 상속해야 합니다.
# 자세한 내용은 ./connections/SUTConnection.py를 참조하십시오.
SUT_connection_file = FIFOConnection.py
[GDB]
path_to_gdb = gdb-multiarch
# address:port 형식으로 작성
gdb_server_address = localhost:4242
[Fuzzer]
# 바이트 단위
maximum_input_length = 100000
# 초 단위
single_run_timeout = 20
# 초 단위
total_runtime = 3600
# 선택 사항
# 각 파일에 하나의 시드가 포함된 디렉토리 경로. 시드를 사용하지 않으려면 값을 비워 두십시오.
seeds_directory =
[BreakpointStrategy]
# 기본 블록을 선택하는 전략은
# 'src/GDBFuzz/breakpoint_strategies/'에 있습니다.
# 논문에서는 다음 전략을 사용합니다.
# 'RandomBasicBlockStrategy.py' - 도달하지 않은 기본 블록을 무작위로 선택
# 'RandomBasicBlockNoDomStrategy.py' - 이전과 동일하지만 지배 관계를 사용하여 전이적으로 도달된 노드를 유도하지 않음
# 'RandomBasicBlockNoCorpusStrategy.py' - 첫 번째와 동일하지만 입력 코퍼스 증가를 방지하므로 커버리지 측정이 포함된 블랙박스 퍼징처럼 동작
# 'BlackboxStrategy.py' - 중단점을 설정하지 않음
breakpoint_strategy_file = RandomBasicBlockStrategy.py
[Dependencies]
path_to_qemu = dependencies/qemu/build/x86_64-linux-user/qemu-x86_64
path_to_ghidra = dependencies/ghidra
[LogsAndVisualizations]
# {DEBUG, INFO, WARNING, ERROR, CRITICAL} 중 하나
loglevel = INFO
# 출력 파일(예: 그래프, 로그 파일)이 저장될 디렉토리 경로.
output_directory = ./output
# True로 설정하면 MQTT 클라이언트가 UI 요소(예: 그래프)를 전송합니다.
enable_UI = False
예제 설정 파일은 ./example_programs/에 있으며, benchmark/benchSUTs/GDBFuzz_wrapper/common/에서 퍼징 하네스를 사용하여 컴파일된 예제 프로그램도 함께 제공됩니다.
다음 명령으로 1시간 동안 퍼징을 시작합니다.
chmod a+x ./example_programs/json-2017-02-12
./src/GDBFuzz/main.py --config ./example_programs/fuzz_json.cfg
먼저 Ghidra가 바이너리 실행 파일을 분석하는 출력이 표시되고, 이후 중단점이 재배치되거나 적중될 때 메시지가 표시됩니다.
설정 파일에 지정된 output_directory에 따라 trial-0 폴더가 생성되며 다음과 같은 구조를 가집니다.
.
├── corpus # 입력 코퍼스가 포함된 폴더.
├── crashes # 크래시 입력(있는 경우)이 포함된 폴더.
├── cfg # 인접 리스트 형태의 제어 흐름 그래프.
├── fuzzer_stats # 퍼징 캠페인의 통계.
├── plot_data # 퍼징 캠페인의 상대적 시간에 도달한 기본 블록을 보여주는 표.
├── reverse_cfg # 역 제어 흐름 그래프.
설정 파일에서 start_ghidra = False로 설정하면 GDBFuzz는 GUI 모드로 실행 중인 Ghidra 인스턴스에 연결합니다. 따라서 ghidra_bridge 플러그인을 스크립트 관리자에서 수동으로 시작해야 합니다. 퍼징 중에는 도달한 프로그램 블록이 녹색으로 강조 표시됩니다.
Linux 사용자 애플리케이션을 퍼징하기 위해 GDBFuzz는 AFL, AFL++, libFuzzer 등 거의 모든 퍼저에서 사용되는 표준 LLVMFuzzOneInput 진입점을 활용합니다.
benchmark/benchSUTs/GDBFuzz_wrapper/common에는 호환되는 모든 퍼즈 하네스를 /tmp/fromGDBFuzz의 명명된 파이프를 통해 입력을 가져오는 독립 실행형 프로그램으로 컴파일하는 데 사용할 수 있는 래퍼가 있습니다.
이를 통해 정의된 입력 인터페이스를 통해 데이터를 소비하는 임베디드 장치를 시뮬레이션하고 모든 애플리케이션에서 GDBFuzz를 실행할 수 있습니다. 편의를 위해 benchmark/benchSUTs에 스크립트를 만들어 나중에 설명된 대로 래퍼를 사용하여 평가의 모든 프로그램을 컴파일합니다.
참고: GDBFuzz는 Linux 사용자 애플리케이션을 퍼징하기 위한 것이 아닙니다. 이를 위해서는 AFL++ 또는 다른 퍼저를 사용하십시오. 래퍼는 규모에 맞는 벤치마크 및 비교 실행을 가능하게 하기 위한 평가 목적으로만 존재합니다!
우리 접근 방식의 일반적인 효율성은 Docker 컨테이너로 배포된 대규모 벤치마크에서 입증되었습니다.
make dockerimage
위 실험을 Docker 컨테이너에서 실행하려면(설정 파일에 지정된 대로 1시간), example_programs 및 output 폴더를 볼륨으로 매핑하고 다음과 같이 GDBFuzz를 시작합니다.
chmod a+x ./example_programs/json-2017-02-12
docker run -it --env CONFIG_FILE=/example_programs/fuzz_json_docker_qemu.cfg -v $(pwd)/example_programs:/example_programs -v $(pwd)/output:/output gdbfuzz:1.0
현재 작업 디렉토리에 위에서 설명한 구조의 출력 폴더가 나타나야 합니다.
우리의 평가는 두 부분으로 나뉩니다.
GDBFuzz는 모든 GDB 서버와 작동할 수 있으므로 대부분의 마이크로컨트롤러 디버그 프로브와 작동합니다.
논문의 RQ1과 관련하여 example_firmware에 있는 다양한 펌웨어를 사용하여 다양한 마이크로컨트롤러에서 GDBFuzz를 실행합니다.
각 실험에 대해 RandomBasicBlock 전략과 RandomBasicBlockNoCorpus 전략으로 GDBFuzz를 실행합니다. 후자는 피드백 없는 퍼징처럼 동작하지만 달성된 커버리지를 측정할 수 있습니다.
RQ1에 답변하기 위해 RandomBasicBlock과 RandomBasicBlockNoCorpus 전략의 달성된 커버리지를 비교합니다.
해당 설정 파일은 해당 하위 폴더에 있으며, 이제 4개의 개발 보드에서 퍼징을 설정하는 방법을 설명합니다.
GDBFuzz는 GDB 서버에 대한 액세스가 필요합니다. 이 경우 B-L4S5I-IOT01A와 온보드 디버거가 사용됩니다. 이 온보드 디버거는 'st-util' 프로그램을 통해 GDB 서버를 설정하고 localhost:4242를 통해 이 GDB 서버에 대한 액세스를 활성화합니다.
sudo apt-get install stlink-tools gdb-multiarch
STM32 B-L4S5I-IOT01A용 펌웨어(예: arduinojson 프로젝트)를 빌드하고 플래시합니다.
전제 조건: platformio (pio) 설치
cd ./example_firmware/stm32_disco_arduinojson/
pio run --target upload
참고: platformio는 SUT의 .elf 파일을 ./example_firmware/stm32_disco_arduinojson/.pio/build/disco_l4s5i_iot01a/firmware.elf에 저장합니다. 이 .elf 파일은 나중에 Ghidra의 사용자 구성에서 사용됩니다.
새 터미널을 시작하고 다음을 실행하여 GDB 서버를 시작합니다.
st-util
arduinojson용 사용자 구성으로 GDBFuzz를 실행합니다. USB 포트를 통해 마이크로컨트롤러로 데이터를 보낼 수 있습니다. 마이크로컨트롤러는 이 데이터를 시리얼을 통해 SUT로 전달합니다. 우리의 경우 /dev/ttyACM0이 마이크로컨트롤러 보드의 USB 장치입니다. 시스템이 마이크로컨트롤러 보드에 다른 장치를 할당한 경우 설정 파일에서 /dev/ttyACM0을 해당 장치로 변경하십시오.
./src/GDBFuzz/main.py --config ./example_firmware/stm32_disco_arduinojson/fuzz_serial_json.cfg
퍼저 통계 및 로그는 ./output/... 디렉토리에 있습니다.
pyocd 설치:
pip install --upgrade pip 'mbed-ls>=1.7.1' 'pyocd>=0.16'
'KitProg v3'가 장치에 있고 적절한 버튼을 눌러 보드를 'Arm DAPLink' 모드로 전환했는지 확인하십시오. GDB 서버 시작: