ABC 미래내일 프로젝트

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

mysticprayer 2026. 5. 3. 22:32

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

재현 및 중복 제거 구현 | 2026.04.27 ~ 05.03

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

📌 1. 금주 학습 목표

  • ONNX 크래시 26건(경량 9 + 심층 17)에 대한 PoC 최소화 및 Triage 입력 작업 완료
  • Crash Reproducer 모듈(전선정 팀원)과 함께 격리 컨테이너 3회 반복 재현 시스템 검증
  • GGUF-004(플레이키 UAF) 케이스의 재현 환경 변수 분석 및 안정 재현 조건 도출
  • 하네스 측 watchdog/timeout 로직 보강으로 무한 루프·교착 상태 자동 차단
  • SafeTensors 포맷 사전 조사 (다음 단계 확장 대비)

📖 2. 학습 및 수행 내용

2-1. ONNX 크래시 PoC 최소화 작업

4주차에 24시간 ONNX 퍼징을 통해 수집한 예비 크래시는 경량 하네스 9건, 심층 하네스 17건으로 총 26건이었다. 이번 주에는 이 26건 각각에 대해 GGUF에서와 동일한 방식으로 LibFuzzer의 -minimize_crash=1 옵션을 활용한 PoC 최소화 작업을 수행하였다.

ONNX 파일은 GGUF와 달리 Protocol Buffers 기반 직렬화 포맷이므로, 단순히 바이트를 잘라내는 것만으로는 유효한 protobuf 메시지를 유지하기 어려운 케이스가 다수 발생하였다. 이를 해결하기 위해 두 단계 최소화 전략을 적용하였다.

# 1단계: LibFuzzer 기본 최소화 (바이트 단위 축소)
./fuzz_onnx_session -minimize_crash=1 \
    -runs=100000 \
    -exact_artifact_path=minimized_stage1.onnx \
    crash_input.onnx

# 2단계: protobuf 구조 인식 최소화 (필드 단위 제거)
python3 scripts/onnx_field_minimizer.py \
    --input minimized_stage1.onnx \
    --output minimized_final.onnx \
    --asan-log original.asan.log

2단계의 필드 단위 최소화 스크립트는 onnx 파이썬 라이브러리로 모델을 파싱한 뒤, 그래프의 각 노드·이니셜라이저를 하나씩 제거해 보면서 동일한 ASan 크래시가 재현되는 최소 부분 그래프를 찾는 방식으로 동작한다.

크래시 ID 유형 원본 크기 최소화 후
ONNX-L-001~009 Protobuf 파싱 (경량) 평균 6.8 KB 평균 184 B
ONNX-D-001~006 Shape Inference 평균 12.4 KB 평균 412 B
ONNX-D-007~012 Type Mismatch 평균 9.1 KB 평균 268 B
ONNX-D-013~017 Memory Corruption 평균 14.7 KB 평균 524 B

2-2. Triage 입력 및 고유 크래시 분류 결과

최소화된 26건의 ONNX 크래시를 Triage 모듈에 입력하여 분류한 결과, 고유 크래시 11건으로 정규화되었다. GGUF의 7건과 합산하면 현재까지 누적 고유 크래시는 총 18건이다.

🔍 Triage 결과의 흥미로운 패턴

심층 하네스에서 발견된 17건 중 Shape Inference 관련 6건이 모두 단 2개의 고유 크래시로 정규화되었다는 점이 인상적이었다. 이는 동일한 근본 원인(root cause)이 다양한 입력 변형을 통해 표면적으로 다르게 보였을 뿐이라는 것을 시사한다. 반면 Memory Corruption 5건은 모두 서로 다른 5개의 고유 크래시로 분류되어, 각각 독립적인 취약점일 가능성이 높다.

2-3. 격리 컨테이너 3회 반복 재현 시스템 검증

전선정 팀원이 구현한 Crash Reproducer 모듈과 함께 첫 종단 간 재현 검증을 수행하였다. 이 모듈은 Triage가 분류한 고유 크래시를 입력받아, 격리된 Docker 컨테이너에서 3회 반복 실행하여 모든 회차에서 동일한 ASan 크래시 시그니처가 재현되는지를 확인한다.

# reproducer/docker-compose.yml - 격리 재현 환경 정의
version: '3.8'
services:
  reproducer:
    image: bugbounty-reproducer:0.5.0
    cap_drop: [ALL]
    cap_add: [SYS_PTRACE]  # ASan 동작에 필요
    security_opt:
      - seccomp:unconfined  # ASan의 시그널 핸들러용
    network_mode: none      # 외부 네트워크 차단
    mem_limit: 2g
    pids_limit: 256
    read_only: true
    tmpfs:
      - /tmp:size=512M
    volumes:
      - ./crashes:/work/crashes:ro
      - ./results:/work/results:rw

하네스 측에서는 재현 검증용 진입점을 별도로 노출시켰다. 기존 LLVMFuzzerTestOneInput과 동일한 로직을 호출하되, 입력은 파일 경로로 받고 종료 코드로 크래시 발생 여부를 반환하는 단순한 래퍼다.

// harness_repro.c - 재현 검증 전용 진입점
int main(int argc, char **argv) {
    if (argc != 2) return 2;
    
    FILE *f = fopen(argv[1], "rb");
    if (!f) return 3;
    
    fseek(f, 0, SEEK_END);
    size_t size = ftell(f);
    fseek(f, 0, SEEK_SET);
    
    uint8_t *buf = malloc(size);
    fread(buf, 1, size, f);
    fclose(f);
    
    // 동일 로직 호출 — ASan이 크래시를 잡으면 SIGABRT
    LLVMFuzzerTestOneInput(buf, size);
    
    free(buf);
    return 0;  // 정상 종료
}
분류 고유 크래시 수 3회 모두 재현 부분 재현 (1~2회) 재현 실패
GGUF 7 5 1 1
ONNX 11 8 2 1
합계 18 13 (72%) 3 (17%) 2 (11%)

3회 모두 안정 재현된 13건은 다음 단계인 Report Generator로 즉시 흘러갈 수 있는 상태가 되었으며, 부분 재현 3건과 실패 2건은 환경 의존성을 분석할 별도 트랙으로 분리하였다.

2-4. GGUF-004 플레이키 UAF 케이스 분석

4주차에 발견된 GGUF-004(Use-After-Free 의심) 케이스는 동일 입력에 대해 일부 환경에서만 재현되는 플레이키 특성을 보였다. 3회 반복 재현 검증에서도 컨테이너 인스턴스에 따라 재현 여부가 달랐으며, 이를 정밀하게 분석할 필요가 있었다.

⚠️ GGUF-004 분석 단계

  • 환경 변수 통제: MALLOC_CONF, ASAN_OPTIONS, LD_PRELOAD 등을 모두 고정하여 메모리 할당기 동작을 일관되게 만듦
  • 할당기 변경 실험: glibc malloc, jemalloc, tcmalloc 각각에서 재현률 측정
  • 스레드 수 통제: OpenMP/pthread 스레드 수를 1로 고정하여 스케줄링 변동 제거
  • ASLR 비활성화: setarch -R로 주소 공간 무작위화를 끄고 재시도
환경 조건 30회 시도 중 재현 횟수 비고
기본 환경 14회 (47%) 플레이키
ASLR 비활성화만 17회 (57%) 미미한 영향
스레드 1개 + ASLR 비활성화 22회 (73%) 개선
jemalloc + 스레드 1개 + ASLR 비활성화 29회 (97%) 안정 재현
MALLOC_CONF=junk:true 추가 30회 (100%) 완전 안정

핵심 발견은 glibc malloc의 fastbin 재사용 패턴이 비결정적 동작을 유발했다는 점이었다. jemalloc으로 교체하고 junk:true 옵션을 적용하면 해제된 메모리 영역이 즉시 마커 패턴으로 채워져, UAF가 결정론적으로 ASan에 검출된다. 이 조건을 표준 재현 환경으로 컨테이너 이미지에 고정하여, GGUF-004는 "안정 재현 가능한 UAF"로 승격되었다.

✓ 플레이키 케이스 처리의 의의

기존 퍼징 도구들이 단순 크래시 탐지에 머무르고 검증 단계에서 "재현 안 됨"으로 누락하는 케이스가 많은데, 본 프로젝트에서는 환경 변수까지 통제 변수로 다뤄 안정 재현 조건을 능동적으로 도출하였다. 이는 1주차 조사에서 정의한 "검증 가능한 취약점만 유효 결과로 확정한다"는 원칙을 실제로 구현한 사례다.

2-5. 하네스 watchdog 및 timeout 로직 보강

장시간 퍼징을 운용하면서 일부 입력이 무한 루프나 교착 상태를 유발하여 워커 하나가 멈추는 현상이 간헐적으로 발생하였다. LibFuzzer 자체의 -timeout 옵션이 있긴 하지만, 일부 시그널 처리 상황에서 동작하지 않는 경우가 있어 하네스 레벨에서 보강이 필요했다.

// harness_watchdog.c - 하네스 내부 watchdog
#include <signal.h>
#include <sys/time.h>

#define HARNESS_TIMEOUT_SEC 10

static void timeout_handler(int sig) {
    (void)sig;
    // 비정상 종료로 LibFuzzer가 timeout으로 인식하도록
    _exit(99);
}

static void arm_watchdog(void) {
    struct sigaction sa = { .sa_handler = timeout_handler };
    sigaction(SIGALRM, &sa, NULL);
    
    struct itimerval timer = {
        .it_value = { .tv_sec = HARNESS_TIMEOUT_SEC, .tv_usec = 0 },
        .it_interval = { 0 }
    };
    setitimer(ITIMER_REAL, &timer, NULL);
}

int LLVMFuzzerTestOneInput(const uint8_t *data, size_t size) {
    arm_watchdog();
    
    // ... 기존 파싱 로직 ...
    
    // 정상 종료 시 타이머 해제
    struct itimerval disable = {0};
    setitimer(ITIMER_REAL, &disable, NULL);
    return 0;
}

이 watchdog을 적용한 후 1시간 단위 안정성 테스트를 수행한 결과, 워커 멈춤(stall) 발생률이 적용 전 4.2%에서 적용 후 0.1% 미만으로 떨어졌다. 또한 timeout으로 종료된 입력 자체도 별도 디렉토리에 수집되어, 추후 "비크래시지만 비정상 동작"을 유발하는 입력으로 분석할 수 있게 되었다.

2-6. SafeTensors 포맷 사전 조사

다음 단계 확장을 대비하여 SafeTensors 포맷에 대한 사전 조사를 진행하였다. SafeTensors는 Hugging Face가 주도하여 만든 AI 모델 저장 포맷으로, "이름 그대로 안전성"을 핵심 가치로 내세운다. pickle 기반 포맷의 임의 코드 실행 위험을 제거하기 위해 설계되었으며, 점차 사실상 표준으로 자리 잡고 있다.

항목 SafeTensors GGUF ONNX
포맷 베이스 JSON 헤더 + 바이너리 커스텀 바이너리 Protocol Buffers
헤더 위치 파일 선두 (길이 prefix) 파일 선두 파일 전체
파서 언어 Rust (safetensors 크레이트) C/C++ (llama.cpp) C++ (onnxruntime)
주요 공격 표면 JSON 파싱 + 오프셋 검증 텐서 메타데이터 검증 그래프 검증 + shape inference

흥미로운 점은 SafeTensors의 공식 파서가 Rust로 구현되어 있어 메모리 안전성 측면에서 GGUF나 ONNX보다 견고하다는 것이다. 그러나 JSON 헤더 파싱 단계, 오프셋·길이 검증 로직, 그리고 다양한 언어 바인딩(Python, JS, Go 등)에서는 여전히 취약점 가능성이 존재한다. 6주차 이후 이 포맷에 대한 하네스를 추가하면, 본 프로젝트가 "주요 AI 모델 포맷 3종을 모두 커버"하는 의미 있는 마일스톤이 될 것이다.

🛠️ 3. 수행 활동

3-1. 개인 수행 활동

  • ONNX 크래시 26건 PoC 최소화 작업 완료 (LibFuzzer 1단계 + protobuf 구조 인식 2단계)
  • protobuf 필드 단위 최소화 스크립트(onnx_field_minimizer.py) 신규 작성
  • 재현 검증용 하네스 진입점(harness_repro.c) 구현 및 컨테이너 이미지 통합
  • GGUF-004 플레이키 UAF 환경 변수 분석 (할당기·스레드·ASLR 통제 실험)
  • jemalloc + junk:true 기반 안정 재현 환경 표준화 및 컨테이너 이미지 반영
  • 하네스 내부 watchdog/timeout 로직 구현 (SIGALRM + setitimer 기반)
  • SafeTensors 포맷 명세 학습 및 비교 분석 자료 작성

3-2. 팀 활동

  • 온라인 정기 미팅 (05.03, Discord): 18건의 누적 고유 크래시 현황과 재현 검증 결과를 공유, 다음 단계인 Report Generator 연동 일정 확정
  • 전선정 팀원과 페어 작업 (04.30~05.01): Crash Reproducer 모듈과의 인터페이스 정합성 검증을 위해 격리 컨테이너 환경에서 직접 함께 디버깅. 특히 ASan 시그널 처리에 필요한 cap_add: SYS_PTRACE 설정을 두고 보안 최소권한 원칙과의 균형점을 논의
  • 신숙우 팀장·황희주 팀원과 인터페이스 협의 (05.02): Report Generator 모듈이 받을 입력 포맷(검증 완료된 ReproducedCrash 구조체) 명세를 확정. 재현 커맨드, 환경 변수 스냅샷, ASan 로그, 최소화된 입력 파일 경로 등이 표준 필드로 포함됨
  • 멘토링 세션 (05.02): 멘토로부터 GGUF-004의 환경 변수 통제 분석이 실무에서 종종 누락되는 중요한 절차라는 피드백을 받음. 또한 SafeTensors 확장 시 공식 Rust 파서뿐 아니라 Python 바인딩(safetensors 패키지의 numpy/torch 변환 경로)도 함께 퍼징 대상에 포함하라는 조언 수령
  • 재현 검증 결과 문서화: Notion에 18건 고유 크래시별 재현 조건, 환경 변수, 최소화된 입력 해시를 정리한 표를 작성하여 팀 전체가 진행 상황을 추적할 수 있도록 공유

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

계획서상 6주차 키워드: "리포트 자동 생성 및 증거 번들링"

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

💬 5. 느낀점 및 회고

5주차는 "발견된 크래시"와 "검증된 취약점" 사이의 거리를 직접 좁혀본 한 주였다. 4주차까지는 하네스가 크래시를 얼마나 많이 만들어낼 수 있는가에 집중했다면, 이번 주는 그 크래시 중 무엇이 진짜 의미 있는 결과인지를 가려내는 단계였다. 26건의 ONNX 크래시가 11건의 고유 크래시로, 다시 8건의 안정 재현 결과로 줄어드는 과정을 보면서, 퍼징의 진짜 가치는 "양"이 아니라 "검증된 양"에 있다는 것을 체감하였다.

GGUF-004의 플레이키 UAF 케이스를 분석한 경험이 가장 인상 깊었다. 처음에는 "재현이 안 되는 크래시는 어차피 못 쓰는 것 아닌가"라는 생각도 들었지만, 환경 변수를 하나씩 통제 변수로 두고 실험을 반복하면서 메모리 할당기의 동작이 결과를 좌우한다는 사실을 발견했다. 결국 jemalloc + junk:true 조합으로 100% 재현 환경을 구축한 순간, "재현되지 않는 것"과 "재현 조건을 아직 모르는 것"이 다르다는 것을 깨달았다. 멘토님이 이 절차를 실무에서도 자주 누락된다고 말씀해 주신 것을 듣고, 이런 작은 디테일들이 결국 결과의 질을 결정한다는 것을 다시 한 번 느꼈다.

전선정 팀원과의 페어 작업도 이번 주의 중요한 경험이었다. Crash Reproducer 모듈의 컨테이너 보안 설정을 두고 "최소권한 원칙"과 "ASan 동작 요건"이 충돌하는 상황을, 둘이 함께 옵션을 하나씩 켜고 끄면서 합의점을 찾아갔다. 코드를 함께 보면서 토론하면 혼자 읽을 때보다 훨씬 빠르게 합리적 결론에 도달한다는 것을 다시 확인했다. 특히 인터페이스가 맞물리는 모듈을 담당하는 팀원과의 페어 세션은, 문서로 주고받는 것보다 훨씬 효율적이라는 것을 체감했다.

watchdog 보강 작업은 사소해 보였지만 효과가 즉각적이었다. 24시간 퍼징 중 워커가 멈추던 4.2%의 손실이 0.1% 미만으로 줄어들면서, 같은 시간을 돌려도 더 많은 신호를 얻을 수 있게 되었다. 4주차의 빌드 자동화에 이어, "기반을 단단히 다지는 작업"이 결국 가장 큰 효율을 만들어낸다는 패턴을 다시 한 번 확인했다. 결국 좋은 시스템은 화려한 기능 하나보다, 안 보이는 곳에서 흔들리지 않는 토대 위에 만들어진다는 점을 이번 주에 새삼 느꼈다.

다음 주는 그동안 검증된 13건의 고유 크래시가 실제 리포트로 자동 생성되는 단계다. 내가 만든 하네스에서 시작된 신호가 Triage를 거쳐 Reproducer로, 다시 Report Generator로 흘러가 최종적으로 사람이 읽을 수 있는 리포트가 되어 나오는 순간을, 4주 전부터 기다려 왔다. 6주차에는 그 첫 완성된 사이클을 직접 보게 될 것 같아, 이번 프로젝트에서 가장 기대되는 한 주가 될 것이다.

📚 6. 참고 자료