[미래내일 일경험] 테크노트 - 3주차
구조 인식 뮤테이터 구현 및 ONNX 하네스 확장 | 2026.04.13 ~ 04.19
| 프로젝트명 | AI 기반 퍼징을 활용한 취약점 탐지 및 검증 자동화 시스템 | ||
| 팀명 | The First | 작성자 | 이우곤 |
| 담당 역할 | 하네스 개발, 퍼징 파이프라인 구축, 포맷 연동 구현 | ||
📌 1. 금주 학습 목표
- GGUF 구조 인식 커스텀 뮤테이터 설계 및 구현 착수
- ONNX 포맷 내부 구조 분석 및 onnxruntime 대상 하네스 프로토타입 작성
- 퍼징 실행 시간 24시간 확장을 통한 깊은 커버리지 확보
- 2주차 예비 크래시 2건에 대한 재현 검증 지원 (전선정 팀원 협업)
- 포맷 연동 인터페이스 표준화를 통한 다중 타깃 확장성 확보
📖 2. 학습 및 수행 내용
2-1. 구조 인식 뮤테이터(Structure-Aware Mutator)의 필요성 재확인
2주차 말미에 수행한 GGUF 하네스 퍼징 결과에서 가장 핵심적으로 드러난 문제는, 단순 바이트 뮤테이션 방식으로는 대부분의 입력이 매직 넘버 검증 단계에서 즉시 기각된다는 점이었다. 24시간으로 확장한 퍼징을 돌리더라도 이 구조적 한계를 넘지 않는 한, 파서의 깊은 로직에 도달하는 입력의 비율은 극히 낮게 유지된다.
이번 주에는 이 문제를 해결하기 위해 LibFuzzer가 제공하는 LLVMFuzzerCustomMutator 인터페이스를 활용한 구조 인식 뮤테이터를 설계하였다. 이 뮤테이터는 GGUF 파일의 구조를 이해하고, 파서가 거부하지 않는 수준의 형식적 유효성을 유지하면서 내부 필드만 변형하는 것을 목표로 한다.
💡 구조 인식 뮤테이터의 설계 원칙
- 불변 필드 보존: 매직 넘버(
GGUF), 지원되는 버전 번호(v3)는 뮤테이션 대상에서 제외하여 파서 진입을 보장 - 경계값 집중 공략: 텐서 개수, 메타데이터 KV 개수, 문자열 길이 필드 등 정수형 필드는 경계값(
0,UINT64_MAX,INT_MAX+1등)으로 집중 변형 - 오프셋/크기 불일치 유도: 텐서 오프셋과 실제 데이터 크기 간의 모순을 의도적으로 생성하여 OOB Read/Write 유도
- LibFuzzer 기존 엔진과 병행: 구조 인식 뮤테이션과 일반 바이트 뮤테이션을 확률적으로 섞어 적용(80:20 비율)하여 예상치 못한 입력 조합도 탐색
2-2. GGUF 구조 인식 뮤테이터 구현
LibFuzzer의 커스텀 뮤테이터 인터페이스는 퍼저가 새 입력을 생성할 때마다 사용자가 정의한 함수를 호출하도록 한다. 구현한 뮤테이터의 핵심 구조는 다음과 같다.
// 커스텀 뮤테이터 엔트리 포인트
size_t LLVMFuzzerCustomMutator(uint8_t *data, size_t size,
size_t max_size, unsigned int seed) {
struct gguf_view view;
if (!parse_gguf_layout(data, size, &view)) {
// 구조 해석 실패 시 LibFuzzer 기본 뮤테이터로 폴백
return LLVMFuzzerMutate(data, size, max_size);
}
// 확률적으로 타깃 필드 선택
int strategy = seed % 5;
switch (strategy) {
case 0: return mutate_tensor_count(data, size, max_size, &view);
case 1: return mutate_kv_string_length(data, size, max_size, &view);
case 2: return mutate_tensor_offset(data, size, max_size, &view);
case 3: return mutate_tensor_dimensions(data, size, max_size, &view);
default: return LLVMFuzzerMutate(data, size, max_size);
}
}
// 텐서 개수 필드를 경계값으로 변형
size_t mutate_tensor_count(uint8_t *data, size_t size,
size_t max_size, struct gguf_view *view) {
static const uint64_t boundary_values[] = {
0, 1, 0xFFFFFFFF, 0xFFFFFFFFFFFFFFFF,
0x80000000, view->tensor_count + 1, view->tensor_count - 1
};
uint64_t new_count = boundary_values[rand() % 7];
memcpy(data + view->tensor_count_offset, &new_count, sizeof(uint64_t));
return size;
}
위 뮤테이터를 기존 하네스에 연결하여 1시간 단위의 짧은 퍼징 세션을 수행한 결과, 2주차 대비 다음과 같은 개선을 확인하였다.
| 지표 | 2주차 (바이트 뮤테이션) | 3주차 (구조 인식) | 증감 |
|---|---|---|---|
| 엣지 커버리지 | 1,847 | 3,412 | +84.7% |
| 유효 입력 비율 | 약 3% | 약 72% | +69%p |
| 예비 크래시 수 | 2건 (10분) | 7건 (1시간) | 시간당 발견율 상승 |
2-3. ONNX 포맷 구조 분석
다음 타깃인 ONNX(Open Neural Network Exchange) 포맷은 GGUF와 달리 Protocol Buffers(이하 protobuf) 기반으로 직렬화된다. 이는 퍼징 관점에서 GGUF와 완전히 다른 접근을 요구한다.
| 비교 항목 | GGUF | ONNX |
|---|---|---|
| 직렬화 방식 | 커스텀 바이너리 포맷 (고정 헤더 + TLV 구조) | Protocol Buffers (가변 길이 태그/필드) |
| 매직 넘버 | 있음 (GGUF) |
없음 (protobuf 필드 태그로 판별) |
| 핵심 취약 지점 | 텐서 오프셋, 문자열 길이 필드 | Shape 불일치, 그래프 노드 간 타입 불일치, 외부 데이터 참조 |
| 파싱 엔트리 | gguf_init_from_file() |
Ort::Session::Session(), onnx::ModelProto::ParseFromArray() |
⚠️ ONNX 퍼징의 기술적 도전 과제
- 두 단계 파싱: onnxruntime은 먼저 protobuf 역직렬화를 수행하고, 이후 그래프 검증 단계에서 shape inference와 타입 체크를 수행한다. 각 단계에 서로 다른 취약점 패턴이 존재한다.
- 순수 바이트 뮤테이션의 한계: 바이트 변형을 가하면 대부분 protobuf 역직렬화 단계에서 실패하여 그래프 검증 로직까지 도달하지 못한다.
libprotobuf-mutator(LPM)를 사용한 프로토콜 인식 뮤테이션이 필수적이다. - 세션 초기화 오버헤드:
Ort::Session생성 자체가 무거운 연산이므로, 순수ModelProto::ParseFromArray()를 먼저 타깃으로 삼아 파싱 단계의 버그부터 집중 탐색하는 이중 하네스 전략을 채택.
2-4. ONNX 퍼징 하네스 프로토타입 구현
ONNX 대상 하네스는 두 가지 레벨로 분리하여 작성하였다. 하나는 protobuf 파싱 단계만 타깃하는 경량 하네스, 다른 하나는 세션 로딩까지 수행하는 심층 하네스이다.
// fuzz_onnx_parse.cc - 경량 하네스 (ModelProto 파싱만 수행)
#include "onnx/onnx.pb.h"
#include <src/libfuzzer/libfuzzer_macro.h>
// libprotobuf-mutator를 활용한 구조 인식 퍼징
DEFINE_PROTO_FUZZER(const onnx::ModelProto& model) {
// 그래프 기본 검증 로직 호출
if (!model.has_graph()) return;
const auto& graph = model.graph();
// 노드 타입 검증을 유도하는 로직
for (const auto& node : graph.node()) {
(void)node.op_type();
for (const auto& attr : node.attribute()) {
(void)attr.name();
}
}
}
// fuzz_onnx_session.cc - 심층 하네스 (Ort::Session 로딩까지 수행)
#include "onnxruntime_cxx_api.h"
extern "C" int LLVMFuzzerTestOneInput(const uint8_t* data, size_t size) {
// 과도하게 큰 모델은 스킵 (퍼징 속도 유지)
if (size > 1024 * 512) return 0;
try {
Ort::Env env(ORT_LOGGING_LEVEL_FATAL, "fuzz");
Ort::SessionOptions opts;
opts.SetIntraOpNumThreads(1);
opts.SetGraphOptimizationLevel(ORT_DISABLE_ALL);
Ort::Session session(env, data, size, opts); // 퍼징 타깃
} catch (const Ort::Exception&) {
// 정상적인 예외는 무시 (크래시만 탐지 대상)
} catch (const std::exception&) {}
return 0;
}
두 하네스의 역할 분담은 다음과 같다.
경량 하네스(fuzz_onnx_parse): libprotobuf-mutator(LPM)의 DEFINE_PROTO_FUZZER 매크로를 사용하여 구조적으로 유효한 ModelProto를 직접 생성한다. 이로써 바이트 레벨 무작위 변형에 쓰이던 시간을 절약하고 의미 있는 입력을 집중 생성한다.
심층 하네스(fuzz_onnx_session): 경량 하네스에서 발견한 흥미로운 시드를 입력으로 삼아, 실제 세션 생성 단계까지 실행한다. shape inference, 타입 체크, 메모리 할당 로직까지 도달하여 RCE 가능성이 있는 심층 취약점을 노린다.
초기 빌드 및 5분간 스모크 테스트 결과, 경량 하네스는 초당 약 1,200회 실행되었고 심층 하네스는 초당 약 35회 실행 속도를 보였다. 심층 하네스의 속도 차이는 크지만, 도달 가능한 코드 영역이 훨씬 넓어 상호 보완적으로 운영할 예정이다.
2-5. 포맷 연동 인터페이스 표준화
GGUF와 ONNX 외에도 향후 SafeTensors, PyTorch(.pt) 등 다양한 포맷을 추가해야 하므로, 각 포맷별 하네스가 공통 인터페이스를 따르도록 표준화 작업을 진행하였다. Rust 코어와 C/C++ 하네스 간의 연동 지점을 단일화하여, 새 포맷 추가 시 코어 수정 없이 하네스만 플러그인 형태로 연결할 수 있는 구조를 설계하였다.
// harness_interface.h - 모든 하네스가 따를 공통 인터페이스
typedef struct {
const char* format_name; // "gguf", "onnx" 등
const char* harness_version;
int (*init)(void);
int (*run_one)(const uint8_t* data, size_t size);
void (*cleanup)(void);
} harness_plugin_t;
// Rust 측 호출 (fuzzer-bridge 크레이트)
// fuzzer-bridge/src/harness.rs
pub trait HarnessPlugin {
fn format_name(&self) -> &str;
fn spawn_fuzzer(&self, config: &FuzzConfig) -> Result<FuzzJobHandle>;
fn collect_artifacts(&self, job: &FuzzJobHandle) -> Result<Vec<CrashArtifact>>;
}
pub struct GgufHarness { /* ... */ }
pub struct OnnxHarness { /* ... */ }
impl HarnessPlugin for GgufHarness { /* ... */ }
impl HarnessPlugin for OnnxHarness { /* ... */ }
🔍 인터페이스 표준화의 이점
이 구조 덕분에 Triage 모듈(전선정 팀원)이나 Report Generator 모듈(신숙우·황희주 팀원)은 하네스의 포맷별 세부사항을 알 필요 없이 CrashArtifact라는 공통 산출물만 소비하면 된다. 팀원 간의 개발 의존성이 줄어들어 병렬 작업이 가능해졌다.
2-6. 24시간 확장 퍼징 실행 및 시드 코퍼스 강화
구조 인식 뮤테이터를 적용한 GGUF 하네스로 24시간 장시간 퍼징을 수행하였다. 퍼징 안정성을 위해 Docker 컨테이너 내부에서 실행하고, -rss_limit_mb=2048, -timeout=60, -fork=4 옵션으로 4개 워커를 병렬 구동하였다.
| 구간 | 실행 횟수 | 엣지 커버리지 | 크래시 누적 |
|---|---|---|---|
| 0~6시간 | 약 1,850만 회 | 4,120 | 14건 |
| 6~12시간 | 약 3,400만 회 | 4,780 | 19건 |
| 12~24시간 | 약 6,800만 회 | 5,103 | 23건 (중복 포함) |
수집된 23건의 크래시는 스택 트레이스 기반으로 1차 중복 제거 시 약 6~8건의 고유 크래시로 추정된다. 정확한 중복 제거는 Triage 모듈(전선정 팀원 담당)에 넘겨 다음 주에 함께 확정할 예정이다. 또한 커버리지를 확장한 입력들은 다음 차수 퍼징을 위한 시드 코퍼스로 자동 편입되었다.
2-7. 2주차 예비 크래시 재현 검증 지원
2주차에 발견한 예비 크래시 2건에 대해 전선정 팀원과 협업하여 재현 검증을 지원하였다. 재현 검증 결과는 다음과 같다.
| 크래시 ID | 오류 유형 | 발생 위치 | 재현 결과 |
|---|---|---|---|
| CRASH-W2-001 | Heap Buffer Overflow | GGUF 메타데이터 문자열 파싱 | ✓ 3/3 재현 성공 |
| CRASH-W2-002 | Assertion Failure | 텐서 타입 검증 | ✗ 재현 실패 (환경 의존성 의심) |
CRASH-W2-001은 안정적으로 재현되어 심층 분석 후보로 확정되었다. 하네스 담당자로서 해당 입력을 최소화(minimize)하는 작업을 수행하여 원본 2.4KB에서 108바이트까지 축소된 PoC를 생성하였다. 이는 이후 리포트에 포함될 재현 증거로 활용된다.
🛠️ 3. 수행 활동
3-1. 개인 수행 활동
- GGUF 구조 인식 커스텀 뮤테이터 설계 및 4종 변형 전략(텐서 개수, KV 문자열 길이, 텐서 오프셋, 텐서 차원) 구현
- ONNX 포맷 명세(onnx.proto) 정독 및 GGUF와의 구조적 차이점 분석
- libprotobuf-mutator(LPM) 빌드 및 통합, 경량/심층 ONNX 하네스 2종 프로토타입 작성
- 하네스 공통 인터페이스(
harness_plugin_t) 설계 및 RustHarnessPlugintrait 정의 - Docker 컨테이너에서 24시간 장시간 퍼징 실행 및 결과 수집
- CRASH-W2-001 PoC 최소화(2.4KB → 108바이트) 수행
- 퍼징 결과 로그 자동 집계를 위한 Python 스크립트 작성
3-2. 팀 활동
- 온라인 정기 미팅 (04.19, Discord): 구조 인식 뮤테이터 적용 전후의 커버리지 비교 결과를 공유하고, ONNX 하네스 이중 구조 전략에 대한 팀원 피드백 수렴
- 오프라인 미팅 (04.20): 청주에서 팀원 전원이 모여 모듈 간 인터페이스(
CrashArtifact구조체)를 확정하고, 각자 맡은 모듈의 진행 상황을 공유 - 전선정 팀원과 협업: 2주차 예비 크래시 2건의 재현 검증을 함께 진행하고, 재현 실패 원인(CRASH-W2-002)의 환경 변수 차이를 분석
- 멘토링 세션 참여: 멘토로부터 구조 인식 뮤테이션 전략에 대한 자문을 받아, 단일 필드 변형뿐 아니라 필드 간 상호 의존성(cross-field)을 깨는 변형도 추후 추가하기로 결정
- GitHub PR 리뷰: 뮤테이터와 ONNX 하네스 PR에 대해 팀장(신숙우) 및 다른 팀원들의 코드 리뷰를 받고 머지 완료
📋 4. 다음 주 계획 (4주차)
- GGUF 뮤테이터에 필드 간 상호 의존성(cross-field) 변형 전략 추가 구현
- ONNX 심층 하네스의 세션 로딩 속도 최적화(세션 풀링, 초기화 비용 감소)
- ONNX 대상 24시간 장시간 퍼징 1차 실행 및 결과 분석
- Triage 모듈(전선정 팀원)과의 인터페이스 연동 테스트 —
CrashArtifact산출 검증 - 하네스 빌드 프로세스 자동화 (CMake + build.rs 통합 빌드 스크립트)
- 3주차에 수집한 GGUF 크래시 6~8건에 대한 PoC 최소화 및 재현 검증 지원
💬 5. 느낀점 및 회고
3주차는 이론과 설계가 실제 성능 향상으로 이어지는 것을 체감한 한 주였다. 2주차에 확인한 "매직 넘버에서 막히는 문제"를 구조 인식 뮤테이터로 해결하자 엣지 커버리지가 84% 상승하고 유효 입력 비율이 3%에서 72%로 치솟았다. 숫자가 극적으로 변하는 것을 직접 눈으로 보니, 퍼징이 단순히 "많이 돌리는 것"이 아니라 "얼마나 의미 있는 입력을 많이 돌리느냐"의 싸움이라는 것이 확실하게 와닿았다.
ONNX 퍼징을 설계하면서 가장 크게 느낀 점은, 포맷이 달라지면 퍼징 전략도 완전히 달라져야 한다는 것이었다. GGUF는 커스텀 바이너리 포맷이라 수동 구조 파싱이 필수였지만, ONNX는 protobuf 기반이라 libprotobuf-mutator라는 기성 도구를 활용할 수 있었다. "포맷의 특성을 읽고 그에 맞는 도구를 선택하는" 감각이 중요하다는 것을 배웠다.
하네스 공통 인터페이스를 설계하면서는 소프트웨어 아키텍처의 힘을 느꼈다. 처음에는 GGUF 하네스 하나만 만들면 될 것 같아 인터페이스를 나중으로 미뤘었는데, ONNX 하네스를 추가하면서 기존 코드와 중복되는 부분이 너무 많아져 결국 인터페이스부터 정의해야 했다. 팀원들과의 모듈 간 결합을 느슨하게 만드는 것은 단순히 코드 품질 문제가 아니라 팀의 개발 속도와 직결된 문제였다.
오프라인 미팅에서 팀원들과 직접 모여 논의하면서, 그동안 비동기 Discord로 주고받던 내용들이 얼마나 많은 오해를 내포하고 있었는지도 깨달았다. 특히 CrashArtifact 구조체의 필드 하나를 두고 40분 넘게 토론한 끝에, 각자가 다른 의미로 사용하고 있었다는 것을 확인하게 되었다. 정기적인 오프라인 미팅의 중요성을 다시 느꼈다.
다음 주에는 ONNX 대상 장시간 퍼징과 Triage 모듈 연동 테스트가 핵심이다. 내가 만든 하네스에서 나오는 크래시들이 실제로 Triage 파이프라인을 타고 검증·분류·리포트까지 이어지는 첫 E2E 플로우를 완성하는 주가 될 것이다.
📚 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기] 프로젝트 2주차 (0) | 2026.04.12 |
| [2026 ABC 프로젝트 멘토링 3기] 프로젝트 1주차 (0) | 2026.04.06 |