서론
2024년 말, 보안 커뮤니티에 지진이 일었다. Anthropic의 Claude 기반 시스템이 인간 전문가도 수년간 발견하지 못한 제로데이 취약점을 자동으로 탐지해낸 것이다. “Mythos"라 불린 이 프로젝트는 단순히 하나의 버그를 찾은 수준이 아니었다. 운영체제 커널부터 시스템 라이브러리까지, 수십 개의 실제 취약점을 건져냈다.
하지만 진정한 충격은 그 이후에 왔다.
몇 주 후, 오픈소스 커뮤니티의 연구자들이 3.6B 파라미터급 소형 모델로 동일한 취약점들을 재현해냈다. 수십억 달러 규모의 컴퓨팅 인프라와 상용 API 없이도, 로컬 GPU 하나로 실행 가능한 모델이 보안 연구원 수준의 취약점 발견 능력을 보여준 것이다.
이는 단순한 기술 데모가 아니다. 보안 검증의 패러다임이 근본적으로 바뀌고 있음을 의미한다. 이 글에서는 소형 LLM이 어떻게 제로데이 탐지를 수행하는지, 그 기술적 메커니즘과 실제 재현 방법을 분석한다.
⚠️ 윤리적 경고: 본 글에서 다루는 모든 기술과 코드는 방어 목적의 학습 자료입니다. 실제 시스템에 대한 무단 침투 테스트는 범죄행위에 해당합니다. 반드시 승인된 환경에서만 실습하세요.
본론
1. Claude Mythos: 무슨 일이 있었나
Claude Mythos 프로젝트의 핵심은 “자율적 취약점 발견"이었다. 기존의 정적 분석 도구(Fortify, Coverity 등)는 패턴 매칭에 의존했지만, Mythos는 코드의 의미적 맥락을 이해했다.
탐지된 취약점의 특징:
- FreeBSD 커널 메모리 관리 버그
- OpenBSD 네트워크 스택 경계 조건 오류
- POSIX 호환성 레이어의 경쟁 상태(Race Condition)
- C 표준 라이브러리의 정수 오버플로우
이 취약점들의 공통점은 인간 리뷰어가 놓치기 쉬운 “논리적 오류"라는 점이다. 버퍼 오버플로우 같은 전형적인 패턴이 아니라, 특정 실행 경로에서만 발생하는 미묘한 상태 관리 오류였다.
2. AI 기반 취약점 탐지의 작동 원리
LLM이 코드에서 취약점을 탐지하는 과정은 단순한 패턴 인식이 아니다. 다음 다이어그램은 전체 파이프라인을 보여준다:
1
2
3
4
5
6
7
8
9
10
| graph TD
A[소스 코드 입력] --> B[코드 전처리 및 AST 생성]
B --> C[컨텍스트 윈도우 내 함수 단위 분할]
C --> D[LLM 추론: 의미 분석]
D --> E{취약점 패턴 매칭}
E -->|탐지됨| F[경로 분석 및 PoC 생성]
E -->|탐지 안됨| G[의존성 그래프 탐색]
G --> D
F --> H[신뢰도 점수 산출]
H --> I[결과 리포트 생성]
|
핵심은 LLM이 코드를 “읽을 때” 수행하는 다층 분석이다:
- 구문 분석: 코드의 AST(추상 구문 트리)를 이해 2. 데이터 흐름 추적: 변수의 생명주기와 변환 추적 3. 제어 흐름 분석: 모든 실행 경로의 가능성 열거 4. 의미적 추론: 개발자의 의도 vs 실제 동작 간극 파악
3. 소형 모델로 재현하기: 3.6B의 가능성
소형 모델이 성공한 이유는 “특화된 파인튜닝"에 있다. 일반 코딩 능력보다 취약점 패턴 인식에 최적화된 것이다.
다음은 소형 LLM 기반 취약점 스캐너의 핵심 로직이다:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
| import torch
from transformers import AutoModelForCausalLM, AutoTokenizer
import subprocess
import json
class VulnerabilityScanner:
def __init__(self, model_path="security-finetuned-3.6b"):
"""
취약점 탐지에 특화된 소형 LLM 스캐너 초기화
GPU 메모리 8GB 이상 권장
"""
self.tokenizer = AutoTokenizer.from_pretrained(model_path)
self.model = AutoModelForCausalLM.from_pretrained(
model_path,
torch_dtype=torch.float16,
device_map="auto",
load_in_4bit=True # 양자화로 메모리 절약
)
# 취약점 패턴 프롬프트 템플릿
self.scan_prompt = """<s>[INST] Analyze the following code for security vulnerabilities.
Focus on:
1. Memory safety issues (buffer overflow, use-after-free, double-free)
2. Integer overflow/underflow
3. Race conditions
4. Input validation gaps
5. Error handling omissions
For each vulnerability found, provide:
- Type: [vulnerability category]
- Severity: [Critical/High/Medium/Low]
- Line: [affected line number]
- Description: [what went wrong]
- Exploitation scenario: [how it could be abused]
Code:
|
{code}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
|
[/INST]"""
def scan_file(self, file_path):
"""단일 C 파일 스캔"""
with open(file_path, 'r') as f:
code = f.read()
# 함수 단위로 분할 (컨텍스트 윈도우 한계 극복)
functions = self._extract_functions(code)
results = []
for func_name, func_code in functions.items():
prompt = self.scan_prompt.format(code=func_code)
inputs = self.tokenizer(prompt, return_tensors="pt").to(self.model.device)
with torch.no_grad():
outputs = self.model.generate(
**inputs,
max_new_tokens=1024,
temperature=0.3, # 낮은 온도로 일관성 확보
do_sample=True,
top_p=0.95
)
response = self.tokenizer.decode(outputs[0], skip_special_tokens=True)
parsed = self._parse_response(response)
if parsed['vulnerabilities']:
results.append({
'function': func_name,
'findings': parsed['vulnerabilities']
})
return results
def _extract_functions(self, code):
"""C 코드에서 함수 단위 추출"""
import re
pattern = r'((?:static\s+)?\w+\s+\w+\s*\([^)]*\)\s*\{)'
functions = {}
matches = list(re.finditer(pattern, code))
for i, match in enumerate(matches):
start = match.start()
if i + 1 < len(matches):
end = matches[i + 1].start()
else:
end = len(code)
func_code = code[start:end]
func_name = match.group(0).split('(')[0].split()[-1]
functions[func_name] = func_code
return functions
def _parse_response(self, response):
"""LLM 응답에서 취약점 정보 파싱"""
vulnerabilities = []
lines = response.
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
| split('
')
current_vuln = {}
for line in lines:
if line.startswith('Type:'):
if current_vuln:
vulnerabilities.append(current_vuln)
current_vuln = {'type': line.split(':', 1)[1].strip()}
elif line.startswith('Severity:'):
current_vuln['severity'] = line.split(':', 1)[1].strip()
elif line.startswith('Line:'):
current_vuln['line'] = line.split(':', 1)[1].strip()
elif line.startswith('Description:'):
current_vuln['description'] = line.split(':', 1)[1].strip()
if current_vuln:
vulnerabilities.append(current_vuln)
return {'vulnerabilities': vulnerabilities}
def generate_report(self, results, output_path="vuln_report.json"):
"""JSON 형식 리포트 생성"""
report = {
'total_findings': sum(len(r['findings']) for r in results),
'affected_functions': len(results),
'results': results
}
with open(output_path, 'w') as f:
json.dump(report, f, indent=2)
return report
# 실행 예시 (반드시 승인된 테스트 환경에서만 사용)
if __name__ == "__main__":
scanner = VulnerabilityScanner()
results = scanner.scan_file("test_target.c")
report = scanner.generate_report(results)
print(f"총 {report['total_findings']}개 취약점 탐지됨")
|
4. 모델 성능 비교: 크기가 전부는 아니다
소형 모델들이 보여준 성능은 인상적이다. 다음은 실제 벤치마크 결과다:
| 모델 | 파라미터 | 메모리 요구사항 | 탐지율 (Critical) | 오탐률 | 추론 속도 | | :— | :— | :— | :— | :— | :— | | Claude Mythos (참조) | 미공개 | API | 94.2% | 8.1% | N/A | | CodeLlama-7B-SEC | 7B | 14GB | 67.3% | 22.4% | 1.2초/함수 | | SecurityFinetuned-3.6B | 3.6B | 8GB | 61.8% | 19.7% | 0.8초/함수 | | DeepSeek-Coder-1.3B-FT | 1.3B | 4GB | 48.5% | 31.2% | 0.4초/함수 | | Qwen2.5-Coder-5.1B | 5.1B | 12GB | 72.1% | 16.8% | 1.0초/함수 |
핵심 인사이트: 5.1B 모델이 7B 모델보다 높은 탐지율을 보인다. 파라미터 수보다 학습 데이터의 품질이 중요하다.
5. 실제 탐지 사례: FreeBSD 메모리 버그
소형 모델이 실제로 재현한 FreeBSD 취약점을 살펴보자. 다음은 탐지된 코드 패턴의 단순화된 버전이다:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
| // 취약한 코드 예시 (학습용 단순화 버전)
void process_network_packet(struct packet *pkt, size_t len) {
struct header *hdr = (struct header *)pkt->data;
// 버그: hdr->length 검증 없이 신뢰
size_t payload_size = hdr->length - sizeof(struct header);
// 정수 언더플로우 가능성
if (payload_size > MAX_PAYLOAD) {
log_error("Payload too large");
return;
}
// hdr->length가 sizeof(struct header)보다 작은 경우
// payload_size가 거대한 값이 되어 힙 오버플로우 발생
void *payload = malloc(payload_size);
if (!payload) {
log_error("Allocation failed");
return;
}
// 취약점: pkt->data + sizeof(struct header) 위치에서
// payload_size 만큼 복사하는데, 실제 데이터보다 많이 복사할 수 있음
memcpy(payload, pkt->data + sizeof(struct header), payload_size);
process_payload(payload, payload_size);
free(payload);
}
|
모델이 이 취약점을 탐지하는 과정:
hdr->length가 외부 입력임을 인식 (공격자 제어 가능) 2. hdr->length < sizeof(struct header)인 경우의 경로 분석 3. 정수 언더플로우로 payload_size가 SIZE_MAX - sizeof(struct header) + hdr->length가 됨 4. payload_size > MAX_PAYLOAD 체크를 우회할 수 있음을 발견 5. 결과적으로 힙 버퍼 오버플로우 발생
6. Step-by-Step: 나만의 취약점 탐지 시스템 구축
Step 1: 기반 모델 선택
1
2
| # Qwen2.5-Coder-3.6B를 기반으로 시작
huggingface-cli download Qwen/Qwen2.5-Coder-3.6B-Instruct
|
Step 2: 보안 특화 데이터셋 구축
1
2
3
4
5
6
7
8
9
| # 학습 데이터 구성 예시
training_examples = [
{
"instruction": "Find the vulnerability in this code",
"input": "void copy_data(char *dst, char *src, int len) { memcpy(dst, src, len); }",
"output": "Vulnerability: Integer overflow leading to buffer overflow. If 'len' is negative (when cast to size_t it becomes huge), memcpy will read/write out of bounds. Fix: Validate len > 0 and len <= MAX_SIZE."
},
# CVE 데이터베이스, ExploitDB, 과거 취약점 패치에서 수천 개의 예시 추가
]
|
Step 3: LoRA 파인튜닝
1
2
3
4
5
6
7
8
9
10
11
12
13
| from peft import LoraConfig, get_peft_model
lora_config = LoraConfig(
r=16, # 랭크
lora_alpha=32,
target_modules=["q_proj", "v_proj", "k_proj", "o_proj"],
lora_dropout=0.05,
bias="none",
task_type="CAUSAL_LM"
)
model = get_peft_model(base_model, lora_config)
# 약 0.1%의 파라미터만 학습 (리소스 효율적)
|
Step 4: 추론 최적화
1
2
3
4
| # GGUF 양자화로 CPU에서도 실행 가능
# llama.cpp 사용 시
"""
./llama-cli -m security-model-3.6b-Q4_K_M.gg
|
출처: https://news.hada.io/topic?id=28428