
하드웨어 브레이크포인트를 활용한 임베디드 시스템 퍼징
이 저장소는 논문 '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 서버 시작:
pyocd gdbserver --persist
펌웨어를 플래시하고 퍼징 시작(예:)
gdb-multiarch
target remote :3333
load ./example_firmware/CY8CKIT_json/mtb-example-psoc6-uart-transmit-receive.elf
monitor reset
./src/GDBFuzz/main.py --config ./example_firmware/CY8CKIT_json/fuzz_serial_json.cfg
ESP32용 펌웨어(예: platformio를 사용한 arduinojson 예제)를 빌드하고 플래시합니다.
cd ./example_firmware/esp32_arduinojson/
pio run --target upload
J-Link 디버거의 openocd 설정 파일에 다음 줄을 추가합니다: jlink.cfg
adapter speed 10000
새 터미널을 시작하고 다음을 실행하여 GDB 서버를 시작합니다.
get_idf
openocd -f interface/jlink.cfg -f target/esp32.cfg -c "telnet_port 7777" -c "gdb_port 8888"
arduinojson용 사용자 구성으로 GDBFuzz를 실행합니다. USB 포트를 통해 마이크로컨트롤러로 데이터를 보낼 수 있습니다. 마이크로컨트롤러는 이 데이터를 시리얼을 통해 SUT로 전달합니다. 우리의 경우 /dev/ttyUSB0이 마이크로컨트롤러 보드의 USB 장치입니다. 시스템이 마이크로컨트롤러 보드에 다른 장치를 할당한 경우 설정 파일에서 /dev/ttyUSB0을 해당 장치로 변경하십시오.
./src/GDBFuzz/main.py --config ./example_firmware/esp32_arduinojson/fuzz_serial.cfg
퍼저 통계 및 로그는 ./output/... 디렉토리에 있습니다.
https://www.ti.com/tool/MSP430-GCC-OPENSOURCE 에서 TI MSP430 GCC 설치
GDB 서버 시작
./gdb_agent_console libmsp430.so
또는 (더 안정적임). https://github.com/dlbeer/mspdebug/ 에서 mspdebug를 빌드하고 다음을 사용합니다.
until mspdebug --fet-skip-close --force-reset tilib "opt gdb_loop True" gdb ; do sleep 1 ; done
Ghidra는 기본적으로 TI MSP430 컨트롤러용 바이너리를 분석하지 못합니다. 이를 해결하려면 Ghidra GUI에서 파일을 가져오고 아키텍처로 MSP430X를 선택한 다음 자동 분석을 건너뜁니다. 그런 다음 'Symbol Table'을 열고 이름별로 정렬한 다음 $C$L*과 같은 이름의 모든 심볼을 삭제합니다. 이제 자동 분석을 실행할 수 있습니다. 분석 후 Ghidra GUI에서 ghidra 브리지를 수동으로 시작한 다음 GDBFuzz를 시작합니다.
./src/GDBFuzz/main.py --config ./example_firmware/msp430_arduinojson/fuzz_serial.cfg
pyusb로 USB 장치에 루트가 아닌 사용자로 액세스하려면 udev에 적절한 규칙을 추가합니다. 다음 줄을 /etc/udev/rules.d/50-myusb.rules에 붙여넣습니다.
SUBSYSTEM=="usb", ATTRS{idVendor}=="1234", ATTRS{idProduct}=="5678" GROUP="usbusers", MODE="666"
udev 다시 로드:
sudo udevadm control --reload
sudo udevadm trigger
논문의 RQ2에서 GDBFuzz를 에뮬레이션 기반 접근 방식인 Fuzzware와 비교합니다. 먼저 제공된 펌웨어 파일에서 이전에 설명한 대로 GDBFuzz와 Fuzzware를 실행합니다. 각 GDBFuzz 실험에 대해 제어 흐름 그래프 파일에서 유효한 기본 블록이 포함된 파일을 다음과 같이 생성합니다.
cut -d " " -f1 ./cfg > valid_bbs.txt
이제 fuzzware 결과에 대한 커버리지를 재생할 수 있습니다. fuzzware genstats --valid-bb-file valid_bbs.txt
크래시 또는 멈춤 입력이 발견되면 crashes 폴더에 저장됩니다. 평가 중에 다음 세 가지 버그를 찾았습니다.
GDBFuzz는 약간의 수정으로 Raspberry Pi 호스트에서도 실행할 수 있습니다.
파일 ./dependencies/ghidra/support/launch.sh:125에서 JAVA_HOME 변수를 예를 들어 JAVA_HOME="/usr/lib/jvm/default-java"로 하드코딩해야 합니다.
다른 보드의 소프트웨어를 퍼징하려면 GDBFuzz에 다음이 필요합니다.
이러한 모든 속성은 설정 파일에 지정되어야 합니다.
RQ 4-8의 경우 대규모 벤치마크를 실행합니다.
먼저 이전에 설명한 대로 Docker 이미지를 빌드하고 benchmark/benchSUTs/GDBFuzz_wrapper/common에 있는 퍼징 하네스를 사용하여 Google의 Fuzzer Test Suite의 애플리케이션을 컴파일합니다.
cd ./benchmark/benchSUTs
chmod a+x setup_benchmark_SUTs.py
make dockerbenchmarkimage
다음으로 benchmark/scripts/benchmark.py 및 benchmark/scripts/benchmark_aflpp.py의 벤치마크 설정을 요구 사항에 맞게 조정하고(특히 number_of_cores, trials, seconds_per_trial), 다음으로 벤치마크를 시작합니다.
cd ./benchmark/scripts
./benchmark.py $(pwd)/../benchSUTs/SUTs/ SUTs.json
./benchmark_aflpp.py $(pwd)/../benchSUTs/SUTs/ SUTs.json
./benchmark/scripts에 각 실험에 대한 플롯 파일(시간에 따른 커버리지), 퍼저 통계 파일, 제어 흐름 그래프 파일이 포함된 폴더가 나타납니다(evaluation/fuzzer_test_suite_qemu_runs와 동일).
GDBFuzz에는 커버된 노드의 제어 흐름 그래프를 플롯하는 선택적 기능이 있습니다. 기본적으로 비활성화되어 있습니다. 이 섹션의 지침을 따르고 사용자 구성에서 'enable_UI'를 'True'로 설정하여 활성화할 수 있습니다.
호스트에서:
설치
sudo apt-get install graphviz
최신 버전의 node를 설치합니다(예: 여기의 Option 2). Option 2를 사용하고 option 1은 사용하지 마십시오. 그러면 node와 npm이 모두 설치됩니다. 참고로, 버전 번호는 다음과 같지만(최신 버전도 작동해야 함):
➜ node --version
v16.9.1
➜ npm --version
7.21.1
웹 UI 종속성 설치:
cd ./src/webui
npm install
mosquitto MQTT 브로커 설치(예: 여기 참조)
mosquitto 브로커 설정 업데이트: /etc/mosquitto/conf.d/mosquitto.conf 파일을 다음 내용으로 바꿉니다.
listener 1883
allow_anonymous true
listener 9001
protocol websockets
mosquitto 브로커 다시 시작:
sudo service mosquitto restart
mosquitto 브로커가 실행 중인지 확인:
sudo service mosquitto status
출력에는 'Active: active (running)' 텍스트가 포함되어야 합니다.
웹 UI 시작:
cd ./src/webui
npm start
웹 브라우저가 자동으로 'http://localhost:3000/'에서 열려야 합니다.
enable_UI가 True로 설정된 사용자 설정 파일을 사용하여 GDBFuzz를 시작합니다. 위의 Docker 컨테이너와 arduinojson SUT를 사용할 수 있습니다. 하지만 'enable_UI'를 'True'로 설정했는지 확인하십시오.
'파란색'으로 커버된 노드는 커버된 것입니다. 흰색 노드는 커버되지 않은 것입니다. 상위 노드가 커버된 경우에만 커버되지 않은 노드를 표시합니다(전체 제어 흐름 그래프를 그리는 것은 제어 흐름 그래프가 큰 경우 너무 오래 걸림).
GDBFuzz는 AGPL-3.0 라이선스에 따라 오픈 소스로 제공됩니다. 자세한 내용은 LICENSE 파일을 참조하십시오.
GDBFuzz에 포함된 다른 오픈 소스 구성 요소 목록은 3rd-party-licenses.txt 파일을 참조하십시오.