
소프트웨어 프록시에서 생성한 테스트 입력으로 CPU 구현을 퍼징한 다음, 실제 하드웨어에서 실행하여 마이크로아키텍처 결함과 정오표를 탐지합니다.
SiliFuzz는 CPU 시뮬레이터나 디스어셈블러 같은 소프트웨어 프록시(proxy)를 퍼징(fuzzing)하여 CPU 결함을 찾고, 그 과정에서 축적된 테스트 입력(이를 *코퍼스(corpus)*라고 함)을 실제 CPU에서 대규모로 실행하는 시스템입니다. SiliFuzz는 진행 중인 작업(work in progress)이며, 자세한 내용은 논문을 참조하세요.
퍼징(Fuzzing)은 대상(애플리케이션 또는 API)을 즉석에서 생성된 대량의 테스트 입력으로 테스트하는 기법입니다. 목표는 코너 케이스(corner case)를 유발하기 위해 이러한 입력을 가능한 한 흥미롭고 다양하게 만드는 것입니다. 즉, 퍼징은 결합된 코드 커버리지(combined code coverage)를 최대화하는 것을 목표로 합니다. 코드 커버리지는 서로 다른 의미를 가질 수 있습니다. 예를 들어 어떤 기본 블록(basic block)이 실행되었는지, 프로그램에서 어떤 경로가 실행되었는지 등을 의미할 수 있습니다.
*프록시(proxy)*는 SiliFuzz의 목적상 대상 CPU의 일부 측면과 유사하게 동작하는 모든 소프트웨어 또는 하드웨어 시스템을 말합니다. 예를 들어 CPU 에뮬레이터나 디스어셈블러가 있습니다. 대상에서 직접 커버리지 정보를 수집할 수 없을 때 프록시가 필요합니다.
퍼징 기법을 프록시에 적용하면 프록시에서 흥미로운 동작을 만들어내는 일련의 테스트 입력(코퍼스)을 생성할 수 있습니다. 우리의 기본 가정은 이것이 대상에서도 유사하게 흥미로운 동작으로 이어진다는 것입니다. 자세한 내용은 문서를 참조하세요.
대상 테스트에 사용되는 입력 모음을 *코퍼스(corpus)*라고 합니다.
상당히 큰 코퍼스는 수백만 개의 입력을 포함하며, 일반적으로 *샤드(shard)*라고 하는 서로 겹치지 않는 여러 청크로 분할됩니다.
SiliFuzz 스냅샷(snapshot)은 짧은 CPU 명령어 시퀀스와 해당 시퀀스를 결정적으로 실행하기 위한 CPU 레지스터 및 메모리의 초기 상태를 설명합니다. 일반적인 스냅샷은 100바이트 미만의 코드를 포함하며 마이크로초 단위로 실행되지만, 임의로 크게 만들 수도 있습니다. 스냅샷은 silifuzz.proto.Snapshot 프로토콜 버퍼로 저장됩니다.
스냅샷은 일반적으로 퍼징 엔진이 생성한 입력으로부터 만들어집니다. CPU 테스트 목적상 이러한 입력은 비결정적(non-deterministic) 스냅샷을 제거하기 위해 필터링됩니다. 자세한 내용은 문서를 참조하세요.
엔드 상태(end state)는 스냅샷 실행이 끝날 때 존재할 것으로 예상되는 레지스터와 메모리의 내용을 설명합니다. 스냅샷이 서로 다른 CPU 마이크로아키텍처에서 다르게 실행된다면 여러 개의 예상 엔드 상태를 갖게 됩니다.
스냅(Snap)은 **러너(Runner)**가 쉽게 로드하고 실행할 수 있도록 메모리에 표현한 스냅샷입니다. 스냅은 일반적으로 *리딩 러너(reading runner)*가 디스크에서 로드합니다. 스냅의 디스크 저장 형식은 기본적으로 메모리 내 형식과 동일하지만, 네이티브 포인터가 오프셋으로 대체된다는 점이 다릅니다. 자세한 내용은 이 헤더를 참조하세요. 이 형식은 종종 *재배치 가능(relocatable)*이라고 합니다. 각 스냅에는 정확히 하나의 예상 엔드 상태가 포함됩니다. 즉, 스냅은 마이크로아키텍처별로 고유합니다. 자세한 내용은 문서를 참조하세요.
러너(Runner)는 단일 CPU 코어를 테스트하기 위한 바이너리입니다. 러너는 코퍼스 샤드를 입력으로 받아 그 안의 무작위 스냅들을 반복적으로 실행하고 예상 엔드 상태에 도달하는지 확인합니다. 러너는 단일 스레드 프로세스입니다.
오케스트레이터(orchestrator)는 여러 러너를 구동하는 프로세스입니다. 일반적인 설정에서 오케스트레이터는 논리 CPU 코어당 하나의 러너를 지속적으로 실행하고, 개별 러너 프로세스가 생성하는 모든 실패를 수집하여 보고합니다.
지원되는 마이크로아키텍처 목록은 이 파일을 참조하세요.
SiliFuzz는 x86_64 및 aarch64 Linux 시스템에서 실행됩니다. Linux 커널 5.x 및 6.x 버전에서 테스트되었으며, 더 오래된 커널 버전과의 호환성은 보장되지 않습니다. 오탐(false positive)을 방지하려면 레거시 vsyscall ABI를 꺼야 합니다.
SiliFuzz가 발견한 버그와 결함의 전체 목록은 아닌 목록입니다.
논리 버그(logic bug)는 특정 CPU 마이크로아키텍처 또는 스테핑(stepping)에 내재된 잘못된 CPU 동작입니다. SiliFuzz가 식별한 버그는 다음과 같습니다:
(전기적) 결함(defect)은 하나 또는 몇 개의 칩에서만 발생하는 잘못된 CPU 동작입니다. SiliFuzz는 논문에서 설명한 다음과 같은 결함을 발견했습니다.
git clone https://github.com/google/silifuzz.git && cd silifuzz
SILIFUZZ_SRC_DIR=`pwd`
./install_build_dependencies.sh # Currently, works for the latest Ubuntu only.
bazel build -c opt @silifuzz//tools:{snap_corpus_tool,fuzz_filter_tool,snap_tool,silifuzz_platform_id,simple_fix_tool_main} \
@silifuzz//runner:reading_runner_main_nolibc \
@silifuzz//orchestrator:silifuzz_orchestrator_main
SILIFUZZ_BIN_DIR=`pwd`/bazel-bin
cd "${SILIFUZZ_BIN_DIR}"
참고: 호스트 시스템을 오염시키지 않으려면 Docker 컨테이너를 사용할 수 있습니다: docker run -it --tty --security-opt seccomp=unconfined --mount type=bind,source=${SILIFUZZ_SRC_DIR},target=/app ubuntu:noble /bin/bash -c "cd /app && ./install_build_dependencies.sh && bazel build ... && bazel test ..."
Bazel의 경우 다음 명령을 사용하세요.
cd "${SILIFUZZ_SRC_DIR}"
COV_FLAGS_FILE="$(bazel info output_base)/external/fuzztest+/centipede/clang-flags.txt"
bazel build -c opt --copt=-UNDEBUG --dynamic_mode=off \
--per_file_copt=unicorn/.*@$(xargs < "${COV_FLAGS_FILE}" |sed -e 's/,/\\,/g' -e 's/ /,/g') @//proxies:unicorn_x86_64
bazel build -c opt @fuzztest//centipede:centipede
mkdir -p /tmp/wd
# Fuzz the Unicorn proxy under Centipede 1000 times with parallelism of 30.
"${SILIFUZZ_BIN_DIR}/external/fuzztest+/centipede/centipede" \
--binary="${SILIFUZZ_BIN_DIR}/proxies/unicorn_x86_64" \
--workdir=/tmp/wd \
-j=30 --num_runs=1000
참고: 퍼징 엔진을 효율적으로 실행하는 방법은 Centipede 문서를 참조하세요.
이 도우미 도구는 현재 실행 중인 머신이 지원되는지 확인하기 위한 것입니다.
$ ${SILIFUZZ_BIN_DIR}/tools/silifuzz_platform_id --short
intel-skylake
참고: SiliFuzz CPU 감지 로직은 지원되는 CPU 중 일부 데스크톱 변형을 인식하지 못합니다. 이러한 경우 도구는 "Unsupported platform"을 보고합니다.
fuzz_filter_tool은 원시 명령어(raw instruction)를 Snap 호환 스냅샷으로 변환합니다. 변환이 가능하면 0을, 그렇지 않으면 1을 반환합니다. 이 인터페이스는 Centipede input_filter와 호환됩니다.
fuzz_filter_tool raw_input_sequence
raw_input_sequence 파일에는 InstructionsToSnapshot을 사용하여 Snapshot 형식으로 변환될 원시 명령어가 들어 있습니다.
사용 예:
# INC EAX
echo -en '\xFF\xC0' > /tmp/inc_eax && ./tools/fuzz_filter_tool /tmp/inc_eax
echo $?
0
snap_tool은 바이너리 Snapshot 프로토를 검사하고 조작합니다. 선택적으로 원시 명령어를 로드하여 Snapshot으로 변환할 수도 있습니다.
echo -en '\xFF\xC0' > /tmp/inc_eax
./tools/snap_tool --raw print /tmp/inc_eax
Metadata:
Id: inc_eax
Architecture: x86_64 Linux
Completeness: complete
Registers:
gregs (non-0 only)
rax = 0x20000000
....
simple fix 도구는 Centipede의 퍼징 결과를 가져와 원시 명령어를 엔드 상태가 없는 스냅샷으로 변환하고, 스냅샷에 엔드 상태를 추가한 다음, 최종적으로 스냅샷을 샤딩된 재배치 가능한 스냅 코퍼스로 패키징합니다.
현재 이 도구는 단일 호스트에서 재시작 불가능한 프로세스로 실행되며 모든 것이 메모리에 저장되므로 처리할 수 있는 코퍼스의 크기는 호스트의 사용 가능한 메모리에 의해 제한됩니다. 엔드 상태가 호스트에서 생성되므로 결과 코퍼스는 단일 아키텍처입니다.
실험적 기능: 해시 테스트(hash test)는 무작위로 생성된 명령어에 엔트로피를 주입하고 그 결과 출력을 가능한 한 효율적으로 캡처하는 무작위 구조화 테스트입니다. 이 접근 방식은 올바른 명령어를 올바른 입력과 함께 호출하면 상당한 비율의 결함을 탐지할 수 있다는 관찰에 기반합니다. 해시 테스트는 이 단순한 결함 클래스를 적극적으로 대상으로 삼아 SiliFuzz와 비교할 실험적 기준점을 제공합니다. 현재 x86_64만 지원됩니다.
예를 들어 Skylake 프로세서가 지원하는 명령어를 포함하는 30k개의 해시 테스트 스냅샷을 /tmp/hashtest 디렉터리에 생성하려면 다음 명령을 실행하면 됩니다.
mkdir -p /tmp/hashtest && bazel run -c opt @silifuzz//fuzzer/hashtest:hashtest_generator -- --platform=intel-skylake -n 30000 --outdir /tmp/hashtest
이 문서의 나머지 부분은 How-to 방식으로 구성되어 있으며, 각 질문은 일반적인 사용 사례 하나를 설명합니다. 질문의 순서는 수행하려는 작업의 복잡성이 점점 증가하는 순서를 나타냅니다. 각 단계는 일반적으로 이전 단계에서 얻은 이해나 산출물(때로는 둘 다)을 요구합니다.
참고: 이 문서는 x86_64 호스트/대상 CPU를 가정합니다. 정확한 출력은 CPU 제조사/스테핑/기타 및 환경(예: Docker/KVM)에 따라 달라질 수 있습니다.
경고: 아래의 많은 지침은 실행 중인 사용자의 권한으로 임의의 바이너리 코드를 실행합니다. 이 도구는 seccomp(2)로 코드를 샌드박싱하기 위해 최선을 다하지만, 사용에 따른 위험은 사용자가 감수해야 합니다.
# INC EAX
$ echo -en '\xFF\xC0' > /tmp/inc_eax
$ ./tools/snap_tool --raw --out=/tmp/inc_eax.pb make /tmp/inc_eax
# CPUID
$ echo -en '\x0F\xA2' > /tmp/cpuid
$ ./tools/snap_tool --raw --out=/tmp/cpuid.pb make /tmp/cpuid
<error log omitted>
Could not load snapshot: INTERNAL: Tracing failed: Banned instruction: CPUID
참고: 비결정적 결과를 피하기 위해 SiliFuzz의 여러 부분에서 특정 명령어 클래스를 제외합니다(예: 위의 CPUID).
$ ./tools/snap_tool print /tmp/inc_eax.pb
Metadata:
Id: inc_eax
Architecture: x86_64 Linux
Completeness: complete
Registers:
gregs (non-0 only):
rax = 0x20000000
rip = 0xeb85c12b000
<omitted>
End states (1):
Endpoint:
Instruction address: 0xeb85c12b002
Platforms:
intel-skylake
Registers (diff vs snapshot's initial values):
gregs (modified only):
rax = 0x20000001
rip = 0xeb85c12b002
<omitted>
RAX 레지스터의 엔드 상태 값이 0x20000001(0x20000000+1, 정확히 INC EAX가 수행하는 연산)인 것을 확인하세요. 또한 RIP 값이 원래 주소 +2인데, 이는 INC EAX 명령어의 크기입니다.
$ ./tools/snap_tool play /tmp/inc_eax.pb
Snapshot played successfully.
참고: 코퍼스를 생성하려면 대상 플랫폼을 지정해야 합니다. 이 예시에서 결과 코퍼스는 생성된 플랫폼을 대상으로 합니다.
$ cd "${SILIFUZZ_BIN_DIR}"
$ PLATFORM_ID=$(./tools/silifuzz_platform_id --short)
$ ./tools/snap_tool generate_corpus /tmp/inc_eax.pb \
--target_platform="${PLATFORM_ID}" > /tmp/inc_eax.corpus
# Will play the same "INC EAX" snapshot 1M times
$ ./runner/reading_runner_main_nolibc /tmp/inc_eax.corpus
gdb로 프로세스를 검사할 수 있습니다:
$ gdb ./runner/reading_runner_main_nolibc
(gdb) b RestoreUContextNoSyscalls
(gdb) run /tmp/inc_eax.corpus
Starting program: .../reading_runner_main_nolibc /tmp/inc_eax.corpus
Breakpoint 1, 0x0000456700010598 in RestoreUContextNoSyscalls ()
(gdb) x/i 0xeb85c12b000 # same as the rip value produced by snap_tool print above
0xeb85c12b000: inc %eax
참고: 이 단계는 앞서 설명한 "Unicorn 대상 퍼징" 단계에 의존합니다.
퍼징 결과 corpus.*를 현재 아키텍처용 10-샤드 실행 가능 코퍼스로 변환합니다.
cd "${SILIFUZZ_BIN_DIR}"
"${SILIFUZZ_BIN_DIR}/tools/simple_fix_tool_main" \
--num_output_shards=10 \
--output_path_prefix=/tmp/wd/runnable-corpus \
--runner="${SILIFUZZ_BIN_DIR}/runner/reading_runner_main_nolibc" \
/tmp/wd/corpus.*
코퍼스 샤드는 /tmp/wd/runnable-corpus.*에 생성됩니다.
$ ./tools/snap_corpus_tool list_snaps /tmp/inc_eax.corpus
...
I0000 00:00:1661887744.019079 4074672 snap_corpus_tool.cc:155] inc_eax
참고: 현재 이 도구는 몇 가지 명령만 제공하는 매우 기본적인 도구입니다.
# Will play the same "INC EAX" snapshot on CPU#1 10k times.
$ ./runner/reading_runner_main_nolibc \
--cpu=1 --num_iterations=10000 /tmp/inc_eax.corpus
오케스트레이터는 --shard_list_file 인자로 전달된 파일에 나열된 모든 샤드를 순환합니다.
$ ls -1 /tmp/wd/runnable-corpus.* > /tmp/wd/shard_list
$ echo 'version: "local_corpus"' > /tmp/wd/corpus_metadata
# Will repeatedly run the corpus on all available CPU cores for 30s using
# /tmp/wd/runnable-corpus.* selected randomly.
$ ${SILIFUZZ_BIN_DIR}/orchestrator/silifuzz_orchestrator_main --duration=30s \
--runner=${SILIFUZZ_BIN_DIR}/runner/reading_runner_main_nolibc \
--shard_list_file=/tmp/wd/shard_list \
--corpus_metadata_file=/tmp/wd/corpus_metadata
참고: 오케스트레이터는 .xz로 끝나는 파일에서 XZ 압축 코퍼스 샤드를 로드할 수도 있습니다.