syz-manager란
syz-manager는 syzkaller 의 메인 프론트엔드 프로그램이다. 전체 퍼징 인프라를 관리하는 중앙 컨트롤러 역할을 한다.
syz-manager는 다음과 같은 주요 역할을 한다.
VM 인스턴스 관리
시작시 다음과 같은 절차를 거친다.
- 설정 파일 읽기
- VM 템플릿 이미지의 스냅샷 생성 (bhyve + ZFS 사용 시)
- 설정된 수만큼 VM 생성
- 각 VM에 SSH로 접속하여 syz-fuzzer 설치 및 시작
Crash 감지 및 처리
vm console 을 모니터링하여 crash 감지 시 다음과 같은 절차를 거친다.
- Crash DB에 추가
- 패닉 메시지로 분류
- 일부 VM을 재현 시도에 할당
- 최소 재현 프로그램 찾기 시도
- C 프로그램으로 변환
- VM 재생성
Corpus Rotation
- VM을 주기적으로 재시작 (crash가 없어도)
- 일부 시스템콜과 corpus 프로그램을 개별 fuzzer에게 숨김
- 목적: 동일한 커버리지를 달성하는 다른 특성의 프로그램 발견
- Local maxima에 갇히는 것을 방지
syz-manager config 파일 분석
{
"target": "linux/amd64",
"http": "0.0.0.0:8080",
"workdir": "/home/user/syzkaller/workdir",
"image": "/home/user/syzkaller/bullseye.img",
"kernel_obj": "/home/user/linux/",
"kernel_src": "/home/user/linux/",
"syzkaller": "/home/user/go/src/github.com/google/syzkaller",
"procs": 4,
"type": "qemu",
"sshkey": "/home/user/syzkaller/bullseye.id_rsa",
"vm": {
"count": 8,
"cpu": 2,
"mem": 2048,
"kernel": "/home/user/linux/arch/x86/boot/bzImage",
"cmdline": "console=ttyS0 root=/dev/sda earlyprintk=serial net.ifnames=0"
}
}
| 필드 | 값 | 설명 |
|---|---|---|
target |
"freebsd/amd64" |
타겟 OS와 아키텍처 |
http |
"0.0.0.0:8080" |
웹 대시보드 바인드 주소 |
workdir |
"/data/syzkaller" |
작업 디렉토리 (코퍼스, 크래시 리포트 저장) |
image |
"/data/syzkaller/bullseye.img" |
VM 루트 파일시스템 이미지 |
syzkaller |
"/home/markj/go/src/..." |
syzkaller 소스 디렉토리 |
kernel_obj |
"/home/user/linux/" |
커널 오브젝트 (심볼 정보용) |
kernel_src |
"/home/user/linux/" |
커널 소스 경로 (커버리지 리포트용) |
procs |
4 |
각 VM 내 퍼저 프로세스 수 |
type |
"qemu" |
하이퍼바이저 종류 |
ssh_user |
"root" |
VM 접속용 SSH 사용자 |
sshkey |
"/data/syzkaller/id_rsa" |
SSH 개인키 경로 |
kernel_obj |
"/usr/obj/.../sys/SYZKALLER" |
커널 오브젝트 파일 (심볼 정보용) |
kernel_src |
"/" |
커널 소스 경로 (커버리지 리포트용) |
vm 설정 상세
| 필드 | 값 | 설명 |
|---|---|---|
bridge |
"bridge0" |
네트워크 브릿지 이름 |
count |
32 |
생성할 VM 개수 |
cpu |
2 |
VM당 가상 CPU 수 |
hostip |
"169.254.0.1" |
호스트 IP (VM과 통신용) |
dataset |
"data/syzkaller" |
ZFS 데이터셋 (bhyve용) |
QEMU 스냅샷 동작 방식
VM 시작 시:
┌────────────────────────┐
│ bullseye.img (원본) │
└───────────┬────────────┘
│ qcow2 copy-on-write
▼
┌────────────────────────┐
│ backing file 설정 │
└───────────┬────────────┘
│ 각 VM마다 오버레이 생성
▼
┌────┬────┬────┬────┐
│VM1 │VM2 │VM3 │... │ ← 독립적인 오버레이 이미지
└────┴────┴────┴────┘
(원본은 읽기 전용, 변경사항만 오버레이에 저장)
VM 재시작 시:
- 오버레이 이미지 삭제
- 새 오버레이 생성
- 깨끗한 상태로 재시작
VM image 준비
syz-manager를 실행하기 전에 VM image 를 준비해야한다.
커널 설정에 필요한 옵션은 다음과 같다.
CONFIG_KCOV=y # 커버리지 수집 (필수)
CONFIG_KCOV_INSTRUMENT_ALL=y # 전체 커널 계측
CONFIG_KCOV_ENABLE_COMPARISONS=y # 비교 연산 추적
CONFIG_DEBUG_INFO=y # 디버그 심볼
CONFIG_KASAN=y # 메모리 오류 감지 (권장)
CONFIG_KMSAN=y # 초기화 안 된 메모리 감지 (권장)
CONFIG_UBSAN=y # 정의되지 않은 동작 감지 (권장)
VM image에 필요한 것은 다음과 같다.
- kcov(4) 활성화된 커널 설치
- root 사용자용 SSH 공개키 설치
- /root/.ssh/authorized_keys
- SSH 서버 활성화
- 네트워크 설정 (DHCP 또는 고정 IP)
커널 빌드 (kcov 활성화)
다음과 같이 커널설정을 해주고 빌드하여 bzImage를 얻는다.
git clone https://github.com/torvalds/linux.git
cd linux
# 커널 설정
make defconfig
make kvm_guest.config
# 필수 옵션 활성화
cat >> .config << EOF
CONFIG_KCOV=y
CONFIG_KCOV_INSTRUMENT_ALL=y
CONFIG_KCOV_ENABLE_COMPARISONS=y
CONFIG_DEBUG_INFO=y
CONFIG_DEBUG_INFO_DWARF4=y
CONFIG_KASAN=y
CONFIG_KASAN_INLINE=y
EOF
# 빌드
make olddefconfig
make -j$(nproc)
루트 파일시스템 이미지를 생성한다. syzkaller는 create-image.sh 스크립트를 제공한다.
wget https://raw.githubusercontent.com/google/syzkaller/master/tools/create-image.sh
chmod +x create-image.sh
./create-image.sh
결과물
drwxr-xr-x. 1 root root 132 12월 10일 15:05 bullseye # 루트 파일시스템 디렉토리 (마운트/추출용)
-rw-------. 1 root root 2590 12월 10일 15:05 bullseye.id_rsa # SSH 개인키 (syz-manager가 VM 접속에 사용)
-rw-r--r--. 1 root root 565 12월 10일 15:05 bullseye.id_rsa.pub # SSH 공개키 (이미지 내 authorized_keys에 등록됨)
-rw-r--r--. 1 root root 2147483648 12월 10일 15:05 bullseye.img # 루트 파일시스템 이미지 (2GB)
리소스 배분 가이드라인
VM 개수 (count)
더 많은 vm 설정은 더 많은 병렬 테스트를 진행한다. 곧 더 빠르게 버그를 찾을 수 있다. (단 호스트 리소스 한계 내에서)
CPU 배분 권장사항
호스트 CPU 수: N 으로 할 때 권장 설정은 다음과 같다.
vm.count × vm.cpu ≤ N × 1~2
예: 16코어 호스트
→ count=8, cpu=2 (총 16 vCPU) ✓
→ count=16, cpu=1 (총 16 vCPU) ✓
→ count=16, cpu=2 (총 32 vCPU) ← 약간 과다
메모리 배분
vm.mem × vm.count ≤ 호스트 RAM - 시스템 예약분
예: 64GB RAM 호스트
→ count=8, mem=4096 (총 32GB) ✓
→ count=16, mem=2048 (총 32GB) ✓
procs (VM당 퍼저 프로세스 수)
procs = VM당 동시 실행 퍼저 수
다중 CPU VM에서:
- procs를 높이면 레이스 컨디션 발견 확률 증가
- 권장: procs ≈ vm.cpu 또는 vm.cpu × 2
실제 퍼징 과정
Corpus 의 개념
- syzkaller의 핵심 영속 상태(persistent state)
- 커널의 다양한 코드 경로를 실행하는 "대표 프로그램들의 집합"
- 퍼저의 시드(seed) 역할
동작 방식
초기 상태: 코퍼스 = ∅ (비어있음)
[코퍼스 성장 과정]
1. 코퍼스에서 프로그램 P 선택
2. P를 약간 변형 → P'
3. P' 실행 후 커버리지 측정
4. 새로운 코드가 실행되었나?
- Yes → P'를 코퍼스에 추가 가능
- No → P' 버림
5. 반복
알고리즘적으로 퍼저가 하는 일은 오직 코퍼스 크기를 늘리는 것이다. 버그 찾기는 이 과정의 부산물이다.
프로그램 변형(Mutation) 종류
퍼저가 새 테스트 프로그램을 생성하는 4가지 방법은 다음과 같다.
| mutation type | description | example |
|---|---|---|
| Splice | 여러 프로그램 결합 | P1 + P2 → P3 |
| Insert | 새 시스템콜 삽입 | [open, read] → [open, mmap, read] |
| Remove | 기존 시스템콜 제거 | [open, read, close] → [open, close] |
| Mutate | 파라미터 값 수정 | read(fd, buf, 100) → read(fd, buf, 4096) |
corpus가 비어있을 때
- 랜덤한 시스템콜 리스트와 랜덤 인자로 새 프로그램 생성
- 코퍼스가 있어도 주기적으로 완전히 새로운 프로그램 생성 (다양성 유지)
VM 내부 구조
┌─────────────────────────────────────────────────────────────┐
│ VM │
│ ┌────────────────────────────────────────────────────────┐ │
│ │ syz-fuzzer │ │
│ │ │ │
│ │ ┌──────────┐ ┌──────────┐ ┌──────────┐ │ │
│ │ │ worker 0 │ │ worker 1 │ │ worker 2 │ ... │ │
│ │ │ goroutine│ │ goroutine│ │ goroutine│ │ │
│ │ └────┬─────┘ └────┬─────┘ └────┬─────┘ │ │
│ │ │ │ │ │ │
│ │ │ shared memory interface │ │ │
│ │ ▼ ▼ ▼ │ │
│ │ ┌──────────┐ ┌──────────┐ ┌──────────┐ │ │
│ │ │syz-exec 0│ │syz-exec 1│ │syz-exec 2│ │ │
│ │ └────┬─────┘ └────┬─────┘ └────┬─────┘ │ │
│ └────────│──────────────│──────────────│──────────────────┘ │
│ │ │ │ │
│ ▼ ▼ ▼ │
│ ┌────────────────────────────────────────────────────────┐ │
│ │ KERNEL │ │
│ │ /dev/kcov │ │
│ └────────────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────────┘
│
RPC (SSH)
│
▼
┌─────────────────┐
│ syz-manager │
│ (호스트에서 실행) │
│ │
│ - 코퍼스 저장 │
│ - 크래시 DB │
│ - 웹 대시보드 │
└─────────────────┘
syz-fuzzer 시작 과정
[syz-manager가 SSH로 syz-fuzzer 시작]
│
▼
┌─────────────────────────────────────┐
│ syz-fuzzer 초기화 │
│ │
│ 1. syz-manager와 RPC 연결 수립 │
│ 2. 작업 큐(work queue) 생성 │
│ 3. Worker goroutine들 생성 │
│ 4. 각 Worker가 syz-executor spawn │
│ 5. 즉시 퍼징 시작 │
└─────────────────────────────────────┘
Worker 스레드의 3가지 특수 작업
(1) Triage (분류/정제)
목적: 새 커버리지를 생성하는 것처럼 보이는 프로그램을 검증하고 정제
[Triage 큐에 프로그램 도착]
│
▼
┌─────────────────────────────────────┐
│ 1. 일관성 검증 │
│ - 같은 프로그램을 여러 번 실행 │
│ - 매번 동일한 커버리지가 나오나? │
│ - No → 버림 (불안정한 커버리지) │
│ │
│ 2. 프로그램 최소화 │
│ - 커버리지를 유지하면서 │
│ - 시스템콜을 하나씩 제거 시도 │
│ - 최소한의 프로그램으로 축소 │
└─────────────────────────────────────┘
│
▼
[Smashing 큐로 이동]
왜 최소화가 중요한가?
- 버그 재현 시 불필요한 노이즈 제거
- 개발자가 버그 원인 파악하기 쉬움
- 예: 100개 시스템콜 → 3개 시스템콜로 축소
(2) Smashing (집중 변형)
목적: Triage를 통과한 "유망한" 프로그램에 집중 투자
[Triage 완료된 프로그램]
│
▼
┌─────────────────────────────────────┐
│ Smashing 단계 │
│ │
│ - 해당 프로그램에 "추가 시간" 투자 │
│ - 집중적으로 다양한 변형 시도 │
│ - 새로운 커버리지 발견 가능성 높음 │
│ │
│ 이유: 이 프로그램이 이미 흥미로운 │
│ 코드 영역에 도달했으므로, 조금만 │
│ 변형해도 더 깊은 코드 탐색 가능 │
└─────────────────────────────────────┘
│
▼
[코퍼스 추가 후보]
(3) Candidate Processing (후보 처리)
목적: syz-manager가 보낸 특별 프로그램 처리
[syz-manager에서 후보 프로그램 도착]
│
▼
┌─────────────────────────────────────┐
│ 후보 프로그램 실행 │
│ │
│ - 실행 후 커버리지 확인 │
│ - 필요시 Triage/Smash 작업 생성 │
│ │
│ 발생 상황: │
│ - syzkaller 재시작 후 기존 코퍼스 │
│ 재평가 필요 │
│ - 커널 업데이트 후 동일 프로그램이 │
│ 다른 결과를 낼 수 있음 │
└─────────────────────────────────────┘
RPC 통신 상세
(1) Poll RPC
syz-fuzzer syz-manager
│ │
│────────── Poll() ─────────────────────>│
│ │
│<─────── 응답 ──────────────────────────│
│ - 최신 코퍼스 스냅샷 │
│ - 글로벌 커버리지 정보 │
│ - 처리할 후보 프로그램들 │
│ │
Poll이 특히 중요한 상황
- syzkaller 재시작 시
- 커널 업데이트 후
- 기존 코퍼스를 새 환경에서 재평가 필요
(2) NewInput RPC
syz-fuzzer syz-manager
│ │
│────── NewInput(triaged_program) ──────>│
│ │
│ ┌───────────────────┤
│ │ 검증: │
│ │ - 코퍼스 크기 제한│
│ │ - 중복 프로그램? │
│ │ - 다른 퍼저가 │
│ │ 이미 발견? │
│ └───────────────────┤
│ │
│<─────── Accept/Reject ─────────────────│
│ │
거부되는 경우
- 코퍼스 크기가 너무 커지는 것 방지
- 다른 VM의 퍼저가 이미 유사한 프로그램 발견
- 중복 커버리지
Corpus Rotation
코드 커버리지는 완벽한 지표가 아니다. 100% 코드 커버리지가 곧 버그 없음은 아니다.
특히 멀티스레드 코드에서
- 동일 코드라도 실행 순서에 따라 버그 발생
- 레이스 컨디션은 커버리지로 감지 불가
corpus rotation을 사용하면 다음과 같은 효과를 볼 수 있다.
- 각 퍼저가 다른 경로로 동일 커버리지 달성 시도
- 중복 노력이 발생하지만, 다양한 특성의 프로그램 발견
- 로컬 최대값 탈출 가능
전체 퍼징 루프 플로우차트
┌─────────────────┐
│ 시작 │
└────────┬────────┘
│
▼
┌─────────────────┐
│ 코퍼스 비어있음? │
└────────┬────────┘
│
┌──────────────┴──────────────┐
│ Yes │ No
▼ ▼
┌─────────────────┐ ┌─────────────────┐
│ 완전히 랜덤한 │ │ 코퍼스에서 │
│ 프로그램 생성 │ │ 프로그램 선택 │
└────────┬────────┘ └────────┬────────┘
│ │
└──────────────┬──────────────┘
│
▼
┌─────────────────┐
│ 변형(Mutation) │
│ - splice │
│ - insert │
│ - remove │
│ - mutate param │
└────────┬────────┘
│
▼
┌─────────────────┐
│ syz-executor로 │
│ 프로그램 실행 │
└────────┬────────┘
│
▼
┌─────────────────┐
│ kcov로 커버리지 │
│ 정보 수집 │
└────────┬────────┘
│
┌──────────────┴──────────────┐
│ │
▼ ▼
┌─────────────────┐ ┌─────────────────┐
│ 크래시 발생? │ │ 새 커버리지? │
└────────┬────────┘ └────────┬────────┘
│ │
┌─────┴─────┐ ┌─────┴─────┐
│Yes │No │Yes │No
▼ │ ▼ │
┌──────────────┐ │ ┌──────────────┐ │
│ 크래시 DB에 │ │ │ Triage 큐에 │ │
│ 기록 │ │ │ 추가 │ │
│ │ │ └──────┬───────┘ │
│ 재현 시도 │ │ │ │
│ │ │ ▼ │
│ 최소화 │ │ ┌──────────────┐ │
│ │ │ │ 일관성 검증 │ │
│ C 코드 생성 │ │ │ 최소화 │ │
└──────────────┘ │ └──────┬───────┘ │
│ │ │
│ ▼ │
│ ┌──────────────┐ │
│ │ Smashing │ │
│ │ (집중 변형) │ │
│ └──────┬───────┘ │
│ │ │
│ ▼ │
│ ┌──────────────┐ │
│ │ NewInput RPC │ │
│ │ 코퍼스 추가 │ │
│ │ 요청 │ │
│ └──────────────┘ │
│ │
└──────────────┬─────────────┘
│
▼
┌──────────────┐
│ 반복... │
└──────────────┘
syz-executor 프로그램 실행
syz-executor의 역할과 특징
- syzkaller에서 실제로 시스템콜을 실행하는 컴포넌트
- C++로 작성됨 (syzkaller의 다른 부분은 Go로 작성)
- syz-fuzzer의 worker 스레드가 spawn
왜 C++인가?
- 시스템콜을 직접 호출해야 함
- 저수준 메모리 조작 필요
- 커널과 직접 상호작용
- 성능이 중요한 부분
┌─────────────────────────────────────────────────────────┐
│ syz-fuzzer (Go) │
│ │
│ ┌──────────┐ ┌──────────┐ ┌──────────┐ │
│ │ worker 0 │ │ worker 1 │ │ worker 2 │ │
│ └────┬─────┘ └────┬─────┘ └────┬─────┘ │
│ │ │ │ │
│ │ spawn │ spawn │ spawn │
│ ▼ ▼ ▼ │
│ ┌──────────┐ ┌──────────┐ ┌──────────┐ │
│ │syz-exec │ │syz-exec │ │syz-exec │ (C++) │
│ │ 0 │ │ 1 │ │ 2 │ │
│ └──────────┘ └──────────┘ └──────────┘ │
└─────────────────────────────────────────────────────────┘
통신 방식: 공유 메모리 인터페이스
왜 공유 메모리인가?
- RPC나 소켓보다 훨씬 빠름
- 대량의 데이터 교환에 적합
- 프로그램 데이터와 커버리지 결과 전달
┌─────────────────┐ ┌─────────────────┐
│ syz-fuzzer │ │ syz-executor │
│ (worker) │ │ │
│ │ │ │
│ ┌───────────┐ │ shared memory │ ┌───────────┐ │
│ │ 프로그램 │──┼───────────────────>│ │ 프로그램 │ │
│ │ (입력) │ │ │ │ 수신 │ │
│ └───────────┘ │ │ └───────────┘ │
│ │ │ │ │
│ │ │ ▼ │
│ │ │ ┌───────────┐ │
│ │ │ │ 시스템콜 │ │
│ │ │ │ 실행 │ │
│ │ │ └───────────┘ │
│ │ │ │ │
│ ┌───────────┐ │ │ ▼ │
│ │ 커버리지 │<─┼────────────────────│ ┌───────────┐ │
│ │ (출력) │ │ │ │ 커버리지 │ │
│ └───────────┘ │ │ │ 결과 │ │
│ │ │ └───────────┘ │
└─────────────────┘ └─────────────────┘
소프트웨어 샌드박스
┌─────────────────────────────────────────────────────────┐
│ syz-executor │
│ │
│ ┌─────────────────────────────────────────────────┐ │
│ │ Sandbox (격리 환경) │ │
│ │ │ │
│ │ ┌─────────────────────────────────────────┐ │ │
│ │ │ 테스트 프로그램 실행 │ │ │
│ │ │ │ │ │
│ │ │ kill(-1, SIGKILL) │ │ │
│ │ │ │ │ │ │
│ │ │ ▼ │ │ │
│ │ │ 샌드박스 내부만 영향 │ │ │
│ │ │ syz-fuzzer는 안전! │ │ │
│ │ └─────────────────────────────────────────┘ │ │
│ │ │ │
│ └─────────────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────┘
샌드박스가 제한하는 것들
- 다른 프로세스에 시그널 보내기
- 파일시스템의 중요 부분 접근
- 네트워크 남용
- 시스템 리소스 고갈
시스템콜 실행 과정
기본 실행 흐름
[프로그램 예시]
───────────────
call[0]: open("/dev/pf", O_RDWR)
call[1]: ioctl(fd0, DIOCRADDTABLES, arg)
call[2]: read(fd0, buf, 100)
call[3]: close(fd0)
1차 실행 (순차 모드)
┌─────────────────────────────────────────────────────────┐
│ Main Thread │
│ │
│ call[0] ──> Thread Pool ──> open() 실행 │
│ │ │ │
│ │ 짧은 대기 │ │
│ ▼ ▼ │
│ call[1] ──> Thread Pool ──> ioctl() 실행 │
│ │ │ │
│ │ 짧은 대기 │ │
│ ▼ ▼ │
│ call[2] ──> Thread Pool ──> read() 실행 │
│ │ │ │
│ │ 짧은 대기 │ │
│ ▼ ▼ │
│ call[3] ──> Thread Pool ──> close() 실행 │
│ │
└─────────────────────────────────────────────────────────┘
시간 ────────────────────────────────────────────────────>
[call0][wait][call1][wait][call2][wait][call3]
왜 대기 시간이 있는가?
- 각 시스템콜이 완료될 시간 확보
- 순차적 실행 보장
- 커버리지 수집의 정확성
Collision Mode (충돌 모드)
레이스 컨디션 버그 발견하기 위한 목적으로 사용한다.
1차 실행 vs Collision Mode:
[1차 실행 - 순차적]
───────────────────
시간 ──────────────────────────────────────>
Thread1: [call0]----[call1]----[call2]----[call3]
Thread2: idle idle idle
커버리지는 측정되지만,
레이스 컨디션은 발생 안 함
[2차 실행 - Collision Mode]
────────────────────────────
시간 ──────────────────────────────────────>
Thread1: [call0]─────[call2]─────
Thread2: [call1]─────[call3]
│
▼
동시 실행으로 레이스 컨디션 유발 가능!
발견할 수 있는 버그 유형
- 락(Lock) 누락
- 이중 해제(Double Free)
- Use-After-Free
- 데이터 손상
- 데드락
syscall(2)을 통한 실행
syscall(2)이란?
- 범용 시스템콜 호출 함수
- 시스템콜 번호와 가변 인자를 받음
// 일반적인 방법
int fd = open("/dev/pf", O_RDWR);
// syscall(2)을 사용하는 방법
int fd = syscall(SYS_open, "/dev/pf", O_RDWR);
// ^^^^^^^^
// 시스템콜 번호
syz-executor의 실행 방식
┌─────────────────────────────────────────────────────────┐
│ syz-executor │
│ │
│ syzkaller 내부 표현: │
│ ┌─────────────────────────────────────────────────┐ │
│ │ { │ │
│ │ syscall_num: 5, // SYS_open │ │
│ │ args: ["/dev/pf", 2] // path, O_RDWR │ │
│ │ } │ │
│ └─────────────────────────────────────────────────┘ │
│ │ │
│ ▼ │
│ ┌─────────────────────────────────────────────────┐ │
│ │ result = syscall(5, "/dev/pf", 2); │ │
│ └─────────────────────────────────────────────────┘ │
│ │ │
│ ▼ │
│ ┌─────────────────────────────────────────────────┐ │
│ │ KERNEL │ │
│ │ │ │
│ │ 시스템콜 번호 5 → open() 핸들러로 라우팅 │ │
│ │ │ │
│ └─────────────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────┘
결과 기록 및 가중치
시스템콜 결과 처리
┌─────────────────────────────────────────────────────────┐
│ 결과 기록 │
│ │
│ syscall() 호출 │
│ │ │
│ ▼ │
│ ┌─────────────┐ │
│ │ 반환값 확인 │ │
│ └──────┬──────┘ │
│ │ │
│ ┌─────┴─────┐ │
│ │ │ │
│ ▼ ▼ │
│ 성공 (≥0) 실패 (<0) │
│ │ │ │
│ ▼ ▼ │
│ 높은 가중치 낮은 가중치 │
│ │
└─────────────────────────────────────────────────────────┘
가중치의 의미
| 결과 | 가중치 | 이유 |
|---|---|---|
| 성공 | 높음 | 커널 깊숙이 실행됨, 더 많은 코드 경로 탐색 |
| 실패 | 낮음 | 초기 검증에서 거부됨, 탐색 깊이가 얕음 |
Triage와 Prioritization에 사용
[프로그램 A]
call[0]: open() → 성공
call[1]: ioctl() → 성공
call[2]: read() → 성공
총 가중치: 높음 ★★★
[프로그램 B]
call[0]: open() → 실패 (ENOENT)
call[1]: ioctl() → 실패 (EBADF)
call[2]: read() → 실패 (EBADF)
총 가중치: 낮음 ★
→ 프로그램 A가 더 "흥미로운" 프로그램으로 판단
→ 변형 시 A를 우선적으로 선택
전체 실행 흐름 요약
┌─────────────────────────────────────────────────────────────┐
│ syz-executor 전체 흐름 │
└─────────────────────────────────────────────────────────────┘
│
▼
┌─────────────────┐
│ 프로세스 시작 │
└────────┬────────┘
│
▼
┌─────────────────┐
│ 스레드 풀 생성 │
└────────┬────────┘
│
▼
┌─────────────────┐
│ /dev/kcov 열기 │
│ 트레이싱 활성화 │
└────────┬────────┘
│
▼
┌─────────────────┐
│ (선택) 샌드박스 │
│ 진입 │
└────────┬────────┘
│
▼
┌─────────────────┐
│ (선택) 디바이스/ │
│ 네트워크 초기화 │
└────────┬────────┘
│
▼
┌───────────────────────────────────────┐
│ 프로그램 실행 루프 │
│ │
│ ┌──────────────────────────────────┐ │
│ │ 1차 실행 (순차 모드) │ │
│ │ │ │
│ │ for each call in program: │ │
│ │ thread = get_idle_thread() │ │
│ │ thread.execute(call) │ │
│ │ wait(short_delay) │ │
│ │ record_result(call) │ │
│ │ │ │
│ └──────────────────────────────────┘ │
│ │ │
│ ▼ │
│ ┌──────────────────────────────────┐ │
│ │ 2차 실행 (Collision Mode) │ │
│ │ │ │
│ │ for each pair in program: │ │
│ │ thread1.execute(call_a) │ │
│ │ thread2.execute(call_b) │ │
│ │ // 대기 없이 즉시 다음 │ │
│ │ │ │
│ └──────────────────────────────────┘ │
│ │
└───────────────────────────────────────┘
│
▼
┌─────────────────┐
│ 커버리지 데이터 │
│ 공유 메모리에 │
│ 기록 │
└────────┬────────┘
│
▼
┌─────────────────┐
│ syz-fuzzer가 │
│ 결과 수집 │
└─────────────────┘
실제 예시: 버그 발견 시나리오
[테스트 프로그램]
─────────────────
call[0]: fd = open("/dev/pf", O_RDWR)
call[1]: ioctl(fd, DIOCRADDTABLES, malformed_arg)
call[2]: close(fd)
[1차 실행 - 순차 모드]
─────────────────────
Thread1: open() → 성공, fd=3
wait...
Thread1: ioctl(3, ...) → 성공 (버그 있는 코드 실행됨)
wait...
Thread1: close(3) → 성공
커버리지: pf 드라이버의 DIOCRADDTABLES 핸들러 실행됨
결과: 크래시 없음 (아직)
[2차 실행 - Collision Mode]
───────────────────────────
Thread1: open() ──────────┐
Thread2: ioctl() ─────────┤ 거의 동시에!
│
▼
레이스 컨디션 발생!
Thread1이 fd를 아직 설정 중인데
Thread2가 ioctl 실행 시도
┌─────────────────────────────────┐
│ KERNEL PANIC! │
│ │
│ Use-after-free in pf driver │
│ at pf_ioctl+0x1234 │
└─────────────────────────────────┘
[크래시 처리]
─────────────
1. syz-manager가 VM 콘솔에서 패닉 감지
2. 크래시 정보를 DB에 기록
3. VM 자동 재생성
4. 재현 시도 시작
5. 최소 재현 프로그램 도출
6. C 코드로 변환하여 개발자에게 제공
트러블 슈팅
다음과 같이 호스트의 GLIBC 버전과 게스트 VM의 GLIBC 버전이 맞지 않아 syzkaller가 구동하지 않을 수 있다.
$ syz-manager -debug -config ./x86.cfg
/syz-executor: /lib/x86_64-linux-gnu/libc.so.6: version `GLIBC_2.38' not found (required by /syz-executor)
/syz-executor: /lib/x86_64-linux-gnu/libc.so.6: version `GLIBC_2.33' not found (required by /syz-executor)
/syz-executor: /lib/x86_64-linux-gnu/libc.so.6: version `GLIBC_2.34' not found (required by /syz-executor)
syz-manager의 경우 호스트에서 빌드해서 호스트에서 쓰지만 syz-executor, syz-fuzzer는 호스트에서 빌드하여 게스트 vm에서 copy하여 실행되기 때문에 이미지를 만들 때 동일한 GLIBC를 사용하는 이미지로 vm을 생성해야한다.
나같은 경우 호스트에서 fedora 43을 사용하고 있어 glibc 2.38+ 을 맞춰 Debian Trixie 이미지를 다운받아서 테스트 했다.
create-image.sh # default Debian Bullseye GLIBC 2.31
create-image.sh -d bookworm # GLIBC 2.36
create-image.sh -d trixie # GLIBC 2.38'Security > Fuzzing' 카테고리의 다른 글
| syzbot 버그 패치 리뷰 (2025.12.18) (0) | 2025.12.18 |
|---|---|
| syzbot, qemu, gdb를 사용하여 linux kernel의 버그 수정 (0) | 2025.12.12 |
| syzkaller를 이용한 커널 퍼징 #4 실제 커널 퍼징 (0) | 2025.12.11 |
| syzkaller를 이용한 커널 퍼징 #2 Syzkaller의 동작 구조 (0) | 2025.12.10 |
| syzkaller를 이용한 커널 퍼징 #1 Fuzzing 이란 (1) | 2025.12.10 |