ABC 미래내일 프로젝트

[2026 ABC 프로젝트 멘토링 3기] 프로젝트 6주차

mysticprayer 2026. 5. 11. 13:05

[미래내일 일경험] 테크노트 - 6주차

리포트 자동 생성 및 증거 번들링 | 2026.05.04 ~ 05.10

프로젝트명 AI 기반 퍼징을 활용한 취약점 탐지 및 검증 자동화 시스템
팀명 The First 작성자 이우곤
담당 역할 하네스 개발, 퍼징 파이프라인 구축, 포맷 연동 구현

📌 1. 금주 학습 목표

  • 안정 재현된 13건의 고유 크래시에 대해 Report Generator 모듈과 함께 HackerOne 표준 양식 리포트 자동 생성 파이프라인 검증
  • 증거 번들 패키징 로직 구현 (입력 파일 + ASan 로그 + 재현 커맨드 + 환경 스냅샷)
  • 부분 재현 3건·재현 실패 2건의 환경 의존성 추가 분석
  • SafeTensors 하네스 프로토타입 착수 (JSON 헤더 + 오프셋 검증 영역 우선)
  • 누적 퍼징 시간 증가에 따른 시드 코퍼스 정리 및 커버리지 정체 영역 분석

📖 2. 학습 및 수행 내용

2-1. Report Generator 모듈 연동 및 첫 자동 리포트 생성

이번 주의 가장 큰 성과는 하네스에서 시작된 신호가 Triage → Reproducer → Report Generator까지 완주하는 첫 종단 간(End-to-End) 사이클을 완성한 것이다. 신숙우 팀장과 황희주 팀원이 구현한 Report Generator 모듈에 5주차에 확정한 ReproducedCrash 구조체를 입력하면, HackerOne 표준 양식의 Markdown 리포트가 자동 생성된다.

하네스 측에서는 Report Generator가 요구하는 추가 메타데이터(컴파일러 버전, 빌드 플래그, 시드 해시 등)를 HarnessFingerprint JSON으로 함께 출력하도록 보강하였다. 이는 리포트의 "재현 가능성" 섹션에 직접 포함되며, 외부 검증자가 동일한 빌드 환경을 재구성할 수 있게 한다.

// harness/fingerprint_gguf.json (빌드 시 생성)
{
  "harness_name": "fuzz_gguf",
  "harness_version": "0.6.0",
  "target": {
    "name": "llama.cpp",
    "commit": "a7b3f29c...",
    "build_date": "2026-05-05T09:12:00Z"
  },
  "toolchain": {
    "compiler": "clang version 17.0.6",
    "flags": ["-O1", "-g", "-fsanitize=fuzzer,address"],
    "sanitizers": ["AddressSanitizer"]
  },
  "runtime_env": {
    "os": "Ubuntu 24.04.2 LTS",
    "kernel": "6.8.0-39-generic",
    "allocator": "jemalloc 5.3.0",
    "malloc_conf": "junk:true"
  }
}

첫 자동 리포트 생성 결과, 안정 재현된 13건 모두 사람이 읽을 수 있는 형태의 리포트로 출력되었다. 각 리포트는 다음 7개 섹션을 포함한다.

섹션 생성 소스 담당 모듈
Summary 크래시 유형 + 심각도 자동 요약 Triage
Steps to Reproduce 재현 커맨드 + 환경 변수 Reproducer
Crash Details ASan 로그 + 정규화 스택 트레이스 Triage
Root Cause Analysis 크래시 위치 + 코드 스니펫 추출 Reporter
Impact RCE 가능성 + CVSS 추정치 Triage + Reporter
Environment HarnessFingerprint JSON Harness
Attachments 증거 번들 zip 링크 + SHA256 Reporter

✓ 첫 완성 사이클의 의미

1주차에 설계도로만 존재했던 "하네스 → 크래시 산출 → Triage → Reproducer → Report Generator"의 5단계 파이프라인이, 6주 만에 실제로 동작하는 시스템으로 완성되었다. 13건의 자동 리포트가 출력되는 순간, 각자 만들던 모듈들이 진정한 의미의 "한 시스템"이 되었음을 확인했다.

2-2. 증거 번들 패키징 로직 구현

버그바운티 제출 시 가장 중요한 것은 "외부 검증자가 동일한 결과를 재현할 수 있는가"이다. 이를 위해 리포트와 함께 첨부될 증거 번들을 zip 아카이브로 자동 패키징하는 로직을 구현하였다. 번들 구조는 다음과 같다.

evidence_GGUF-001_20260507.zip
├── report.md                    # HackerOne 표준 양식 본 리포트
├── crash_input.bin              # 최소화된 PoC 입력 (108 B)
├── crash_input.sha256           # 입력 파일 해시
├── asan.log                     # ASan 크래시 로그 원본
├── stack_trace.normalized.txt   # Triage가 정규화한 스택 트레이스
├── reproduce.sh                 # 재현 셸 스크립트 (한 줄 실행)
├── env.snapshot.json            # 컨테이너 환경 스냅샷
├── harness_fingerprint.json     # 빌드 시 생성된 환경 지문
└── Dockerfile.repro             # 재현 환경 Dockerfile (자체 완결)

핵심은 reproduce.sh 한 줄로 누구나 동일한 크래시를 재현할 수 있도록 만든 것이다. 스크립트는 Dockerfile.repro로 이미지를 빌드하고, 컨테이너 내부에서 env.snapshot.json의 환경 변수를 그대로 적용한 뒤, 첨부된 PoC를 하네스에 입력하여 ASan 로그를 출력한다.

#!/usr/bin/env bash
# reproduce.sh - 외부 검증자용 한 줄 재현 스크립트
set -euo pipefail

BUNDLE_DIR="$(dirname "$(readlink -f "$0")")"
cd "$BUNDLE_DIR"

# 1. 무결성 검증
sha256sum -c crash_input.sha256

# 2. 재현 환경 이미지 빌드
docker build -t bb-repro:local -f Dockerfile.repro .

# 3. 환경 변수를 적용하여 하네스 실행
docker run --rm \
  --cap-drop=ALL --cap-add=SYS_PTRACE \
  --network=none --read-only \
  --env-file env.snapshot.env \
  -v "$BUNDLE_DIR/crash_input.bin:/work/input.bin:ro" \
  bb-repro:local /work/harness_repro /work/input.bin

echo "[+] 재현 완료. 위 ASan 출력을 첨부된 asan.log와 비교하세요."

⚠️ 번들 설계 시 신경 쓴 부분

  • 자체 완결성: 외부 인터넷 의존 없이 번들 안의 파일만으로 재현 가능 (Dockerfile 베이스 이미지만 예외)
  • 무결성 검증: PoC 입력 파일의 SHA256을 별도 기록하여 전송 중 변조 탐지
  • 최소권한 컨테이너: 5주차에 합의한 cap_drop=ALL + cap_add=SYS_PTRACE 패턴을 재현 환경에도 그대로 적용
  • 정보 분리: 민감할 수 있는 빌드 머신 식별자(호스트명, 사용자명)는 HarnessFingerprint에서 제외

2-3. 부분 재현·재현 실패 케이스 추가 분석

5주차에 별도 트랙으로 분리한 부분 재현 3건과 재현 실패 2건에 대해 GGUF-004 분석에서 사용한 환경 변수 통제 방법론을 동일하게 적용하였다. 결과를 정리하면 다음과 같다.

크래시 ID 최초 상태 발견된 의존 요소 최종 상태
GGUF-005 부분 재현 (2/3) 스택 크기 (ulimit -s) 의존 안정 재현 승격
ONNX-D-009 부분 재현 (1/3) CPU 아키텍처 의존 (AVX2 명령어) 조건부 재현
ONNX-D-014 부분 재현 (2/3) 스레드 스케줄링 (race condition 의심) 분석 진행 중
GGUF-007 재현 실패 시드 코퍼스 의존 (특정 캐시 상태 필요) 아티팩트 추정
ONNX-D-016 재현 실패 미확정 (재시도 결과 일관성 없음) 보류

분석 결과, 부분 재현 3건 중 GGUF-005가 안정 재현으로 승격되어 리포트 가능 크래시는 13건에서 14건으로 증가하였다. ONNX-D-009는 AVX2 지원 CPU에서만 재현되는 "조건부 재현"으로 분류해 리포트에 환경 요구사항을 명시하기로 했다. ONNX-D-014의 race condition 의심 케이스는 별도 분석이 더 필요하며, GGUF-007은 시드 코퍼스 캐시 상태에 의존하는 비유효 아티팩트로 판단해 제외하였다.

2-4. SafeTensors 하네스 프로토타입 착수

5주차에 사전 조사한 SafeTensors 포맷에 대한 하네스 프로토타입 개발에 착수하였다. 공식 파서가 Rust로 작성되어 있어 GGUF·ONNX와 달리 Rust 네이티브 퍼징 도구인 cargo-fuzz를 활용하는 방향으로 설계하였다.

// fuzz_targets/fuzz_safetensors.rs - cargo-fuzz 기반 하네스
#![no_main]
use libfuzzer_sys::fuzz_target;
use safetensors::SafeTensors;

fuzz_target!(|data: &[u8]| {
    // 1. 헤더 파싱 단계 (JSON + 길이 prefix)
    if data.len() < 8 { return; }
    
    // 2. SafeTensors 역직렬화 시도
    //    내부적으로 JSON 헤더 파싱 + 텐서 오프셋 검증 수행
    match SafeTensors::deserialize(data) {
        Ok(st) => {
            // 3. 모든 텐서에 접근하여 lazy 로딩 경로까지 도달
            for name in st.names() {
                let _ = st.tensor(name);
            }
        }
        Err(_) => {
            // 정상적인 거부는 무시
        }
    }
});

멘토링에서 받은 조언대로, Rust 공식 파서뿐 아니라 Python 바인딩(safetensors 패키지)의 numpy/torch 변환 경로도 별도 하네스로 추가하기로 했다. Python 바인딩 하네스는 7주차에 본격 구현 예정이다.

첫 1시간 시범 퍼징 결과는 다음과 같다.

측정 항목 결과 비고
실행 속도 약 220회/초 Rust 인프로세스 퍼징의 효율
엣지 커버리지 (1h) 2,108 JSON 파싱 + 오프셋 검증 위주
예비 크래시 / 패닉 0건 (Panic 4건) 메모리 안전 위반 없음, panic은 별도 분석

🔍 Rust 파서 퍼징의 특이점

예상대로 C/C++ 파서에서 흔히 보이던 메모리 커럽션 류 크래시는 1시간 퍼징에서 0건이었다. 대신 Rust의 panic!으로 인한 의도적 중단이 4건 발견되었는데, 이는 보안 취약점이라기보다 DoS 가능성 정도로 평가된다. 다만 패닉이 외부 입력으로 트리거된다는 점에서, 라이브러리 사용자에게는 여전히 의미 있는 신호다. SafeTensors는 "메모리 안전성은 강력하지만 입력 검증은 별도로 다뤄야 한다"는 Rust 보안 모델의 전형을 보여 준다.

2-5. 시드 코퍼스 정리 및 커버리지 정체 영역 분석

누적 퍼징 시간이 GGUF는 약 120시간, ONNX는 약 96시간에 도달하면서 시드 코퍼스가 크게 비대해지고 커버리지 증가가 정체되는 현상이 두드러졌다. 코퍼스를 분석한 결과, 중복에 가까운 입력이 다수 누적되어 퍼저가 비슷한 경로만 반복해서 탐색하고 있음을 확인했다.

LibFuzzer의 -merge=1 모드를 활용해 코퍼스 미니마이제이션을 수행하였다. 이 모드는 동일한 커버리지를 유지하는 최소 입력 집합만 남기는 작업으로, 시간이 다소 걸리지만 효과는 즉각적이다.

포맷 정리 전 코퍼스 정리 후 코퍼스 감소율
GGUF 48,210 개 (총 2.4 GB) 3,847 개 (총 198 MB) -92.0% (개수) / -91.7% (용량)
ONNX 31,406 개 (총 1.8 GB) 2,512 개 (총 142 MB) -92.0% (개수) / -92.1% (용량)

정리된 코퍼스로 1시간 추가 퍼징을 수행한 결과, 엣지 커버리지가 GGUF 5,341 → 5,612 (+271), ONNX 12,420 → 12,798 (+378)로 다시 증가하기 시작했다. 코퍼스 정리 자체가 새로운 경로 탐색을 촉진한다는 것을 확인하였다.

추가로, llvm-cov를 활용해 커버되지 않은 코드 영역을 시각화한 결과 다음 두 영역이 정체 구간으로 식별되었다.

  • GGUF 양자화 메타데이터 처리 분기: Q4_K, Q5_K 등 양자화 타입별 메타데이터 처리 로직 진입률이 8% 미만 → 양자화 타입을 명시적으로 변형하는 뮤테이션 전략 추가 필요
  • ONNX Custom Operator 등록 경로: 사용자 정의 연산자 처리 경로 진입률이 3% 미만 → 시드 코퍼스에 custom op를 포함한 모델 추가 필요

🛠️ 3. 수행 활동

3-1. 개인 수행 활동

  • 하네스 측 HarnessFingerprint JSON 출력 기능 신규 구현 (빌드 시 자동 생성)
  • Report Generator 모듈과의 인터페이스 디버깅 및 자동 리포트 생성 검증 (14건 최종 출력)
  • 증거 번들 zip 패키징 로직 구현 (reproduce.sh + Dockerfile.repro 자동 생성)
  • 부분 재현 3건·재현 실패 2건의 환경 의존성 분석 (스택 크기, AVX2, race condition 등)
  • SafeTensors 하네스 프로토타입 작성 (cargo-fuzz 기반) 및 1시간 시범 퍼징
  • GGUF·ONNX 시드 코퍼스 미니마이제이션 작업 수행 (-merge=1)
  • llvm-cov를 활용한 미커버 영역 시각화 및 다음 단계 뮤테이션 전략 도출

3-2. 팀 활동

  • 온라인 정기 미팅 (05.10, Discord): 14건의 자동 리포트 생성 결과를 공유하고, HackerOne 제출 후보 선별 기준에 대해 논의. 심각도 High 이상의 4건(GGUF-001, 003, ONNX-D-001, 004)을 우선 제출 후보로 분류
  • 신숙우 팀장·황희주 팀원과 페어 작업 (05.06~05.07): Report Generator의 Markdown 출력 형식을 HackerOne 공식 템플릿과 비교하며 누락된 필드(reporter contact, disclosure timeline 등) 보강. 7개 섹션 구조를 최종 확정
  • 전선정 팀원과 협업 (05.08): 부분 재현 케이스의 환경 의존성 분석을 함께 수행. 특히 ONNX-D-014의 race condition 의심 케이스는 ThreadSanitizer(TSan)로 추가 분석하는 방향을 합의
  • 멘토링 세션 (05.09): 멘토로부터 증거 번들의 reproduce.sh 한 줄 재현 설계가 실제 버그바운티 보고에서 큰 강점이라는 피드백을 받음. 또한 SafeTensors의 panic 케이스도 라이브러리 의존 서비스에서는 의미 있는 DoS 신호라는 점에서 별도 카테고리로 리포트하라는 조언 수령
  • HackerOne 정책 검토: 팀 전체가 HackerOne의 책임 있는 공개(Coordinated Disclosure) 정책과 llama.cpp·onnxruntime 프로젝트의 보안 정책(SECURITY.md)을 함께 검토. 제출 전 최소 90일 비공개 기간 준수 등 절차를 확인

📋 4. 다음 주 계획 (7주차)

계획서상 7주차 키워드: "취약점 우선순위 선정 및 1차 제출 준비"

  • High 심각도 4건(GGUF-001, 003, ONNX-D-001, 004)에 대한 RCE 가능성 정밀 분석 (레지스터 상태 검토)
  • 우선 제출 후보 리포트의 본문 인간 검토 및 보안적 표현 다듬기
  • llama.cpp·onnxruntime 프로젝트의 보안 정책 절차에 따른 1차 제출 진행
  • SafeTensors Python 바인딩 하네스 추가 구현 (numpy/torch 변환 경로)
  • GGUF 양자화 메타데이터 뮤테이션 전략 추가 (Q4_K, Q5_K 분기 도달)
  • ONNX-D-014 race condition 의심 케이스 ThreadSanitizer 분석 (전선정 팀원과 협업)

💬 5. 느낀점 및 회고

6주차는 "파이프라인이 마침내 한 바퀴 완주한 한 주"였다. 1주차에 노션에 그렸던 5단계 모듈 흐름도가, 6주가 지나 실제로 동작하는 시스템이 되어 14건의 사람이 읽을 수 있는 리포트로 출력되는 것을 보았을 때, 그동안의 모든 디버깅과 인터페이스 협의가 한 점에 모이는 듯한 느낌이었다. 특히 내가 만든 하네스에서 시작된 신호가 Triage → Reproducer → Report Generator를 거쳐 최종 리포트가 되어 나오는 순간은, 이번 프로젝트에서 가장 강렬한 성취감을 느낀 장면이었다.

증거 번들 설계 작업을 하면서 "재현 가능성"이 보안 보고의 본질이라는 것을 새삼 깨달았다. 처음에는 단순히 입력 파일과 ASan 로그만 첨부하면 된다고 생각했지만, 외부 검증자의 입장에서 생각해 보면 환경 변수 하나, 컴파일러 플래그 하나가 결과를 바꿀 수 있다. 그래서 Dockerfile.repro까지 번들에 포함하여 "이 zip 하나만 받으면 누구나 동일한 결과를 재현할 수 있다"는 자체 완결성을 만든 것이 가장 만족스러운 설계였다. 멘토님이 reproduce.sh 한 줄 재현이 실무에서 큰 강점이라고 평가해 주신 점도 인상적이었다.

부분 재현 케이스를 다시 분석하면서, 5주차의 GGUF-004 분석 방법론이 일종의 재사용 가능한 도구가 되었다는 것을 느꼈다. 처음 GGUF-004를 분석할 때는 시행착오가 많았지만, 그때 정리한 "할당기 → 스레드 → ASLR → 기타 환경 변수" 통제 순서를 GGUF-005, ONNX-D-009에 똑같이 적용하니 훨씬 빠르게 결론에 도달했다. 한 번 어렵게 길을 낸 분석 방법이, 다음 케이스에서는 정해진 절차가 된다는 점에서 "프로세스를 만드는 일"의 가치를 다시 한 번 체감했다.

SafeTensors 하네스를 작성하면서 "파서 언어가 보안 모델 자체를 바꾼다"는 것을 직접 체감했다. C/C++ 파서에서는 가장 흔하게 보던 메모리 커럽션이 Rust 파서에서는 1시간 동안 0건이었고, 대신 panic이라는 다른 형태의 신호가 나왔다. 같은 "퍼징"이라는 행위가 타깃에 따라 전혀 다른 종류의 발견을 만들어낸다는 것이 흥미로웠다. 동시에 Rust라고 해서 입력 검증 문제가 사라지는 것은 아니라는 점, 그리고 Python 바인딩처럼 안전한 코어를 감싸는 외피에 또 다른 공격 표면이 존재할 수 있다는 점은 다음 주의 중요한 탐구 과제가 될 것이다.

시드 코퍼스 정리 작업은 이번 주의 "작지만 큰 변화"였다. 92%나 되는 코퍼스를 삭제했음에도 커버리지가 다시 증가하기 시작한 것은, "더 많은 데이터가 더 좋은 결과를 만든다"는 직관이 항상 옳지 않다는 것을 보여 줬다. 좋은 시드는 다양성 있는 소수의 입력이지, 비슷한 입력 수만 개가 아니라는 것을 데이터로 확인했다. 4주차의 빌드 자동화, 5주차의 watchdog 보강에 이어 이번 주의 코퍼스 정리까지, "보이지 않는 곳을 다듬는 작업"이 결국 시스템 전체의 효율을 만든다는 패턴이 점점 더 분명해진다.

다음 주는 드디어 실제 외부 프로젝트에 취약점을 제출하는 단계다. 학교 과제나 내부 테스트가 아닌, 실제 사용 중인 오픈소스 프로젝트의 보안 정책 절차를 따라 우리가 발견한 결과를 공식적으로 제출하게 된다. 이 과정에서 우리의 리포트가 외부의 객관적인 평가를 받게 될 것이고, 그 피드백은 앞으로의 분석 방향에도 큰 영향을 줄 것이다. 6주 전에는 단지 "퍼징을 해 보자"였던 프로젝트가, 7주차에는 실제 보안 생태계에 기여하는 결과물로 이어진다는 사실이 가장 의미 있게 다가오는 한 주가 될 것 같다.

📚 6. 참고 자료