[미래내일 일경험] 테크노트 - 2주차
시스템 아키텍처 설계 및 하네스 프로토타입 개발 | 2026.04.06 ~ 04.12
| 프로젝트명 | AI 기반 퍼징을 활용한 취약점 탐지 및 검증 자동화 시스템 | ||
| 팀명 | The First | 작성자 | 이우곤 |
| 담당 역할 | 하네스 개발, 퍼징 파이프라인 구축, 포맷 연동 구현 | ||
📌 1. 금주 학습 목표
- 전체 시스템 아키텍처 설계 및 모듈 구조도 작성
- GGUF 포맷 대상 퍼징 하네스 프로토타입 구현
- LibFuzzer 기반 퍼징 실행 환경 구축 및 첫 퍼징 수행
- Rust 프로젝트 스캐폴딩 및 Cargo workspace 초기 구성
- Rust-C FFI 연동 검증
📖 2. 학습 및 수행 내용
2-1. 시스템 아키텍처 설계
1주차에 조사한 기술 스택과 파이프라인 구조를 바탕으로, 전체 시스템의 아키텍처를 구체적으로 설계하였다. 시스템은 크게 4개의 핵심 모듈로 구성된다.
① Fuzzing Engine 모듈
LibFuzzer 기반의 퍼징 실행 엔진으로, 구조 인식 뮤테이터와 커버리지 수집기를 포함한다. 타깃별 하네스를 로드하여 퍼징을 수행하며, 크래시 발생 시 해당 입력을 자동으로 저장한다. C/C++로 구현한다.
② Crash Reproducer 모듈
발견된 크래시를 격리된 Docker 컨테이너에서 3회 반복 재현하여 유효성을 검증하는 모듈이다. 재현 실패 시 환경 차이 로그를 기록하고, 재현 성공 시 ASan 로그와 함께 다음 단계로 전달한다. 전선정 팀원이 주로 담당한다.
③ Triage & Analysis 모듈
재현된 크래시의 스택 트레이스를 정규화하여 중복을 제거하고, 메모리 오염 유형(힙 오버플로우, UAF, 스택 오버플로우 등)과 레지스터 상태를 분석하여 RCE 가능성 등 심각도를 자동 분류한다. 전선정 팀원이 담당한다.
④ Report Generator 모듈
검증이 완료된 취약점에 대해 HackerOne 표준 양식의 버그바운티 리포트를 자동 생성한다. 재현 커맨드, 입력 파일 해시, ASan 로그, 스택 트레이스 등을 하나의 증거 번들로 패키징한다. 신숙우 팀장과 황희주 팀원이 담당한다.
이 네 모듈을 연결하는 중앙 제어 역할은 Rust 기반 Job Queue가 담당한다. tokio 비동기 런타임 위에서 퍼징 작업을 스케줄링하고, 각 모듈 간 데이터 흐름을 관리한다.
💡 설계 시 고려한 핵심 원칙
- 모듈 간 느슨한 결합: 각 모듈은 파일 시스템(크래시 디렉토리, 리포트 디렉토리) 기반으로 데이터를 주고받아 독립적으로 개발 및 테스트 가능
- 장애 격리: 퍼징 중 타깃 바이너리의 무한 루프나 메모리 초과가 발생해도 Watchdog이 프로세스를 강제 종료하여 플랫폼 전체의 안정성 유지
- 확장성: 새로운 타깃 포맷(SafeTensors 등)을 추가할 때 하네스와 뮤테이터만 추가하면 나머지 파이프라인은 재사용 가능하도록 설계
2-2. GGUF 포맷 구조 분석 및 하네스 설계
하네스 프로토타입 작성에 앞서 GGUF 파일 포맷의 내부 구조를 상세히 분석하였다. GGUF는 llama.cpp 프로젝트에서 사용하는 AI 모델 저장 포맷으로, 다음과 같은 계층 구조를 갖는다.
| 영역 | 주요 필드 | 퍼징 관점의 의미 |
|---|---|---|
| 매직 넘버 | GGUF (4바이트) |
파일 유효성 검증의 첫 관문. 뮤테이션 시 보존해야 파서 깊은 로직까지 도달 가능 |
| 헤더 | 버전, 텐서 개수, 메타데이터 KV 개수 | 텐서 개수와 실제 데이터 간 불일치를 유도하면 경계 조건 버그 탐지 가능 |
| 메타데이터 KV | 모델명, 아키텍처, 양자화 정보 등 | 문자열 길이 필드 조작으로 버퍼 오버리드/오버플로우 유발 가능성 |
| 텐서 정보 | 이름, 차원, 타입, 오프셋 | 오프셋 값 조작으로 범위 밖 읽기(OOB Read) 유도 가능 |
| 정렬 패딩 | 32바이트 경계 정렬 | 정렬 오프셋 계산 오류 시 잘못된 메모리 접근 발생 가능 |
| 텐서 데이터 | 실제 가중치 바이너리 | 크기가 매우 크므로 최소 시드에서는 축소하여 퍼징 효율 확보 |
이 분석을 바탕으로 하네스의 퍼징 진입점을 gguf_init_from_file() 함수로 선정하였다. 이 함수는 GGUF 파일을 메모리에 로드하고 파싱하는 핵심 함수로, 헤더 파싱부터 텐서 메타데이터 읽기까지 전체 파싱 로직을 포함한다.
2-3. GGUF 퍼징 하네스 프로토타입 구현
LibFuzzer 기반의 GGUF 퍼징 하네스 프로토타입을 C로 작성하였다. 핵심 구조는 다음과 같다.
int LLVMFuzzerTestOneInput(const uint8_t *data, size_t size) {
// 1. 퍼저가 생성한 데이터를 임시 파일로 저장
char tmpfile[] = "/tmp/fuzz_gguf_XXXXXX";
int fd = mkstemp(tmpfile);
write(fd, data, size);
close(fd);
// 2. GGUF 파싱 함수 호출 (퍼징 대상)
struct gguf_init_params params = { .no_alloc = true, .ctx = NULL };
struct gguf_context *ctx = gguf_init_from_file(tmpfile, params);
// 3. 리소스 정리
if (ctx) gguf_free(ctx);
unlink(tmpfile);
return 0;
}
⚠️ 하네스 작성 시 주의사항
- 메모리 누수 방지: 매 퍼징 반복마다
gguf_free()로 할당된 메모리를 해제해야 한다. 누수가 누적되면 ASan이 OOM으로 오탐할 수 있다. - 임시 파일 관리:
gguf_init_from_file()은 파일 경로를 인자로 받으므로 퍼저 데이터를 임시 파일로 기록 후 전달한다. 매 반복 후unlink()로 삭제하여 디스크 공간 고갈을 방지한다. - no_alloc 옵션: 텐서 데이터 전체를 메모리에 로드하면 퍼징 속도가 현저히 저하되므로,
no_alloc = true로 설정하여 메타데이터 파싱만 수행하도록 한다.
2-4. LibFuzzer 빌드 및 첫 퍼징 실행
작성한 하네스를 LibFuzzer + ASan으로 빌드하고, 첫 퍼징을 수행하였다. 빌드 명령은 다음과 같다.
# llama.cpp 소스와 함께 하네스 빌드
clang++ -g -O1 -fsanitize=fuzzer,address \
-I./include -I./ggml/include \
fuzz_gguf.c ./src/ggml.c ./src/gguf.c \
-o fuzz_gguf
# 시드 코퍼스 디렉토리 구성
mkdir -p corpus/gguf
# 최소 유효 GGUF 파일을 시드로 배치
cp minimal_test.gguf corpus/gguf/
# 퍼징 실행 (최대 메모리 2GB, 입력 크기 제한 100KB)
./fuzz_gguf corpus/gguf/ -max_len=102400 -rss_limit_mb=2048
약 10분간의 초기 퍼징 결과를 요약하면 다음과 같다.
| 항목 | 결과 | 비고 |
|---|---|---|
| 총 실행 횟수 | 약 85,000회 | 인프로세스 퍼징으로 초당 약 140회 실행 |
| 커버리지 증가 | 엣지 커버리지 312 → 1,847 | 시드 코퍼스 투입 후 빠르게 증가 |
| 크래시 발견 | 2건 (예비) | 헤더 파싱 단계에서 발생, 추후 재현 검증 필요 |
| 주요 발견 | 매직 넘버 보존 필요 확인 | 매직 넘버가 깨진 입력은 파서가 즉시 거부하여 깊은 로직 도달 불가 |
🔍 초기 퍼징에서 얻은 인사이트
단순 바이트 뮤테이션만으로는 대부분의 입력이 매직 넘버 검증 단계에서 걸러진다. 이는 1주차에 조사한 구조 인식 뮤테이터의 필요성을 실제 데이터로 확인한 것이다. 3주차부터는 GGUF 헤더의 매직 넘버와 버전 필드를 보존하면서 나머지 영역만 변형하는 커스텀 뮤테이터를 구현할 계획이다.
2-5. Rust 프로젝트 초기 구성 및 FFI 연동 검증
플랫폼 코어가 될 Rust 프로젝트의 초기 구조를 Cargo workspace로 구성하였다.
bugbounty-platform/
├── Cargo.toml # workspace 루트
├── crates/
│ ├── core/ # Job Queue, 스케줄러, Watchdog
│ │ ├── Cargo.toml
│ │ └── src/lib.rs
│ ├── fuzzer-bridge/ # C 하네스 FFI 바인딩
│ │ ├── Cargo.toml
│ │ ├── build.rs # bindgen으로 C 헤더 바인딩 자동 생성
│ │ └── src/lib.rs
│ ├── triage/ # 크래시 분석, 스택 정규화
│ │ ├── Cargo.toml
│ │ └── src/lib.rs
│ └── reporter/ # 리포트 생성
│ ├── Cargo.toml
│ └── src/lib.rs
├── harnesses/ # C/C++ 하네스 소스
│ ├── gguf/
│ │ └── fuzz_gguf.c
│ └── onnx/ # (추후 구현)
└── scripts/ # Python 보조 스크립트
FFI 연동 검증을 위해 fuzzer-bridge 크레이트에서 간단한 C 함수를 호출하는 테스트를 수행하였다.
// build.rs - bindgen으로 C 헤더 바인딩 생성
fn main() {
let bindings = bindgen::Builder::default()
.header("wrapper.h")
.generate()
.expect("Unable to generate bindings");
let out_path = PathBuf::from(env::var("OUT_DIR").unwrap());
bindings.write_to_file(out_path.join("bindings.rs"))
.expect("Couldn't write bindings");
}
// src/lib.rs - FFI 호출 테스트
mod ffi {
include!(concat!(env!("OUT_DIR"), "/bindings.rs"));
}
pub fn validate_gguf_magic(data: &[u8]) -> bool {
data.len() >= 4 && &data[..4] == b"GGUF"
}
cargo test를 실행하여 Rust에서 C 함수 호출이 정상적으로 이루어지는 것을 확인하였다. 이로써 Rust 코어 ↔ C 하네스 간의 기본적인 통신 경로가 검증되었다.
🛠️ 3. 수행 활동
3-1. 개인 수행 활동
- llama.cpp 소스코드 중 GGUF 파싱 관련 코드(
gguf.c,ggml.c) 정독 및 분석 - GGUF 최소 유효 시드 파일 직접 제작 (매직 넘버 + 버전 + 빈 메타데이터 + 빈 텐서)
- LibFuzzer + ASan 빌드 환경 구축 (Clang 17, LLVM 17 설치 및 설정)
- GGUF 퍼징 하네스 프로토타입 작성 및 첫 퍼징 실행
- Cargo workspace 초기 프로젝트 구조 설계 및 생성
- bindgen 크레이트를 활용한 FFI 바인딩 생성 테스트 수행
3-2. 팀 활동
- 온라인 정기 미팅 (04.12, Discord): 시스템 아키텍처 설계안을 공유하고 팀원들의 피드백을 반영하여 모듈 간 인터페이스를 확정
- 아키텍처 설계 문서 작성: Notion에 전체 시스템 구조도, 모듈별 책임 범위, 데이터 흐름도를 정리하여 공유
- GitHub 프로젝트 보드 구성: 이슈 템플릿, PR 규칙, 브랜치 전략(Git Flow)을 설정하고 각 팀원의 담당 이슈를 등록
- 초기 퍼징 결과 공유: 첫 퍼징에서 발견된 2건의 예비 크래시를 팀에 공유하고, 재현 검증 계획을 논의
📋 4. 다음 주 계획 (3주차)
- GGUF 구조 인식 커스텀 뮤테이터 설계 및 구현 착수 (매직 넘버/버전 보존, 메타데이터 영역 집중 변형)
- ONNX 포맷 분석 및 onnxruntime 대상 하네스 프로토타입 작성
- Rust Job Queue 코어 로직 구현 시작 (tokio 기반 비동기 작업 스케줄러)
- 예비 크래시 2건에 대한 재현 테스트 수행 (전선정 팀원과 협업)
- 퍼징 실행 시간을 24시간으로 확장하여 더 깊은 커버리지 확보
💬 5. 느낀점 및 회고
2주차는 1주차의 이론 조사를 바탕으로 실제 코드를 작성하고 퍼징을 실행해 본 첫 주였다. 가장 의미 있었던 경험은 GGUF 하네스를 직접 작성하고 LibFuzzer로 첫 퍼징을 돌려본 것이다. 수십 초 만에 커버리지가 빠르게 올라가는 것을 보면서 커버리지 유도 퍼징의 효과를 체감할 수 있었다.
동시에 한계도 명확히 드러났다. 대부분의 입력이 매직 넘버 검증에서 즉시 거부되어 파서의 깊은 로직까지 도달하지 못하는 문제가 있었다. 이 문제는 구조 인식 뮤테이터를 통해 해결해야 하며, 3주차의 핵심 과제가 될 것이다.
Rust 프로젝트 구조를 설계하면서 Cargo workspace의 편리함을 느꼈다. 여러 크레이트를 하나의 워크스페이스에서 관리하면서도 각 모듈을 독립적으로 빌드하고 테스트할 수 있어, 팀원들이 병렬로 개발을 진행하기에 적합한 구조라고 생각한다. FFI 연동도 bindgen 덕분에 예상보다 수월하게 진행되었다.
이번 주를 통해 "설계 → 구현 → 실행 → 피드백"의 한 사이클을 빠르게 돌려본 것이 큰 수확이다. 다음 주부터는 이 피드백을 바탕으로 뮤테이터를 고도화하고 ONNX 포맷으로 퍼징 대상을 확장할 예정이다.
📚 6. 참고 자료
'ABC 미래내일 프로젝트' 카테고리의 다른 글
| [2026 ABC 프로젝트 멘토링 3기] 프로젝트 6주차 (0) | 2026.05.11 |
|---|---|
| [2026 ABC 프로젝트 멘토링 3기] 프로젝트 5주차 (0) | 2026.05.03 |
| [2026 ABC 프로젝트 멘토링 3기] 프로젝트 4주차 (1) | 2026.04.26 |
| [2026 ABC 프로젝트 멘토링 3기] 프로젝트 3주차 (0) | 2026.04.20 |
| [2026 ABC 프로젝트 멘토링 3기] 프로젝트 1주차 (0) | 2026.04.06 |