나만의 도구는 어디까지 갈 수 있을까 — LLM 기반 취약점 스캐너 DMASS 개발기

이군·2026년 4월 22일

호기심 하나로 시작했다

AI 시대가 되면서 "도구를 만든다"는 일의 무게가 확연히 가벼워졌다. 예전 같으면 한 달은 족히 걸릴 프로토타입이 하루 이틀이면 돌아가고, 문서와 스펙을 조각조각 이어 붙여야 했던 아키텍처 설계가 대화 몇 번으로 정리된다. 그렇다면 자연스러운 질문이 따라온다.

"내가 혼자 만든 도구는 어느 정도의 성능을 낼 수 있을까?"

특히 나는 한 가지가 궁금했다. 보안 스캐너처럼 "정답이 딱 정해져 있지 않고, 상황마다 판단이 필요한" 영역에서도 과연 혼자 만든 도구가 쓸 만한 수준에 도달할 수 있을까. 상용 스캐너들은 수십 명의 보안 연구자가 수년에 걸쳐 시그니처와 휴리스틱을 쌓아 올린 결과물이다. 그걸 혼자서, 그것도 LLM이라는 새 기둥을 중심에 놓고 다시 만들어보면 무엇이 나올까. 나와 비슷한 시도를 한 것처럼 보이는 여러 오픈소스를 써봤지만 뭔가 하나씩 아쉬움이 있었고, 기법들만 차용해서 LLM을 활용해서 LLM 기반의 소스코드 스캐너를 만들어보면 어떨까하는 생각을 했다.

그 호기심에서 시작된 결과물이 DMASS (Dual-Mode Autonomous Security Scanner) 다. 이 글은 그 개발기다.


기존 스캐너가 답답했던 지점

오픈소스든 상용이든 기존 DAST(Dynamic Application Security Testing) 스캐너들은 대체로 같은 패턴이다.

고정된 페이로드 테이블 → HTTP 요청 → 정규식/시그니처 매칭 → Finding

이 구조는 빠르고 재현성이 좋지만, 세 가지 한계가 오래 지적되어 왔다.

  1. 컨텍스트를 못 본다. /api/users/1 에 공격을 시도할 때, 이 엔드포인트가 어떤 인증을 요구하는지, 반환 값이 SPA 캐치올인지 진짜 API 응답인지, 같은 테스트를 user_a와 user_b 세션으로 비교해야 의미 있는지 — 이런 판단이 전부 사람이 설계해둔 규칙에 종속된다.
  2. False Positive가 많다. Juice Shop 같은 SPA는 존재하지 않는 경로도 200 OK에 HTML을 주는데, 대부분의 스캐너는 이걸 보고 "엔드포인트 발견"이라고 외친다.
  3. 체인 공격을 못 본다. "SSRF로 cloud metadata를 읽어 IAM role 탈취 → 그 role로 S3 접근"이라는 이야기를, 개별 취약점만 뱉는 스캐너는 절대 들려주지 못한다.

LLM은 이 세 가지 한계에 묘하게 잘 들어맞는다. 컨텍스트를 읽고, 가설을 세우고, 여러 신호를 종합해서 "이건 진짜인지 가짜인지" 판단하는 건 — 돌이켜 보면 LLM이 가장 잘하는 일이다. 문제는 "그걸 실제로 스캐너로 만들 수 있느냐"였다.


핵심 설계: 스캐너가 아니라 에이전트다

DMASS의 가장 근본적인 결정은 "진단을 수행하는 주체가 LLM이다"라는 역할 전환이었다. 기존 구조는 프로그램이 취약점 여부를 판정하고 LLM은 설명문이나 써주는 보조였다. DMASS는 반대로 만들었다.

1. LLM에게 타겟 컨텍스트 + 툴 목록 전달
2. LLM이 어떤 probe를 할지 스스로 결정
3. LLM이 tool_use로 http_request / timing_baseline / issue_oob_token 등 호출
4. 툴 실행 결과를 LLM에 돌려줌
5. LLM이 응답을 분석하고 다음 probe 결정
6. 확실하면 record_finding, 의심이면 추가 probe
7. conclude_diagnosis로 스스로 종료

코드 관점에서는 "에이전틱 루프"지만, 설계 관점에서는 "모의해커의 사고 흐름을 그대로 에뮬레이트한다" 가 훨씬 정확하다. 사람 펜테스터가 실제로 하는 일을 떠올려 보자. 타겟을 보고, 가설을 세우고, 작은 probe를 하나 날려 보고, 응답을 보고 가설을 갱신하고, 다음 probe를 결정한다. 확신이 들면 적고, 아니면 계속 판다. LLM에게 이 루프를 그대로 맡겼다.

LLM이 호출할 수 있도록 제공한 툴은 여덟 개다.

용도
http_request스코프 검증된 HTTP 호출
timing_baselineN회 요청 시간 측정 → 평균·표준편차·threshold
compare_responses두 요청 통계 비교 (길이·상태)
issue_oob_tokenBlind 취약점용 OOB 토큰 발급
check_oob_callback토큰에 콜백 도착 여부 조회
get_session_infoprimary/user_b/admin 세션 쿠키
record_finding확정된 취약점 기록
conclude_diagnosis진단 자발적 종료

특히 timing_baseline 은 구현하며 설계의 묘미를 느낀 툴이었다. 시간 기반 SQL Injection을 탐지하려면 "이 응답이 평소보다 느린가?"를 판단해야 한다. 고정 threshold를 박아 두면 네트워크 RTT에 따라 전부 깨진다. 그래서 LLM이 필요할 때마다 baseline을 측정해서 stddev를 계산하게 하고, 그 수치를 기반으로 "5초 sleep payload가 실제로 5초 더 걸렸는가"를 본인이 판단하게 만들었다. 휴리스틱을 프로그램이 아니라 LLM이 동적으로 만든다는 느낌이 여기서 가장 강했다.


화이트박스와 블랙박스, 그리고 그 사이

처음엔 블랙박스(URL만 주고 스캔)만 만들려 했다. 그런데 만들다 보니 두 가지가 눈에 밟혔다.

  • 소스코드를 가진 상태에서 블랙박스만 돌리는 건 정보 낭비다. grep만 해봐도 엔드포인트 목록이 나오고, req.query.id가 어느 DB 쿼리로 흘러가는지가 보인다.
  • 반대로 화이트박스 정적 분석만으로는 "실제로 공격 가능한가"를 절대 확인할 수 없다. Taint flow가 깔끔해 보여도 WAF 앞단에서 막히면 무의미하다.

그래서 세 모드를 다 지원하게 했다.

  • --repo: 화이트박스. 소스코드 읽어서 엔드포인트 추출, taint 분석, secrets 스캔, 의존성 취약점(OSV API).
  • --url: 블랙박스. 동적 엔드포인트 발견, WAF 탐지, LLM 에이전트로 실제 probe.
  • --repo + --url: 하이브리드. 양쪽 결과를 합성해서 "이 IDOR은 실제 코드 라인 42의 findById(req.params.id) 에서 온 것" 같은 식으로 증거를 연결.

하이브리드 모드에서 가장 재미있었던 건 chain-synthesizer 였다. 정적 규칙 9개로 잘 알려진 체인 패턴(예: SSRF→Cloud Metadata)을 잡고, 그 뒤에 LLM에게 "이것들 말고 네가 보기에 비표준 체인이 있다면 최대 3개 제안해봐"라고 창의력을 요청한다. 실제로 Juice Shop 스캔에서 이 LLM 창의 제안이 뱉은 체인 중 하나가 이거였다.

Attack Chain (LLM-proposed): DATABASE_COMPROMISE_VIA_SQLI_AND_CONFIG_ACCESS — CVSS 10.0
발견된 SQL Injection과 인증 없이 접근 가능한 admin config 엔드포인트를 연결하면, DB 덤프 후 config에서 얻은 자격증명으로 다른 리소스 접근까지 이어진다.

이게 정적 규칙이었으면 못 잡았을 체인이다. 두 finding은 서로 다른 에이전트가 서로 다른 시점에 발견한 것이었고, "함께 놓고 보면" 엄청난 이야기가 된다는 건 LLM이 판단했다.


현실과 부딪친 지점들

개발기는 성공담만으로 채울 수 없다. 만들면서 실제로 부딪친 네 가지 문제와 그 해결 과정을 적는다.

1. LLM이 스코프를 넘어간다

가장 먼저 겪은 문제. LLM은 신이 나면 example.com 이나 google.com 같은 외부 도메인으로 probe를 날린다. 시나리오상 "서드파티 통합이 있는지 확인하자"라는 논리적 흐름이긴 하지만, 실제 스캔에서는 이건 명백한 스코프 위반이다.

해결은 모든 HTTP 요청이 반드시 RequestCoordinator 를 거치게 한 것. 그 안에 ScopeGuard가 in_scope / out_of_scope를 검증하고, 위반 시 silent skip 없이 throw 한다. LLM은 툴 결과로 "ScopeViolationError" 를 받고, 다음 probe에서 알아서 스코프 안쪽으로 회귀한다. 이 설계 하나로 스코프 이탈 문제가 거의 사라졌다. "LLM을 믿지 말고 경계에서 강제하라"는 원칙을 체감한 사건이었다.

2. 비용이 폭발한다

Agentic 루프는 본질적으로 비용이 비싸다. 매 스텝마다 이전 모든 tool_result가 컨텍스트에 들어가기 때문에, 루프가 12번 돌면 마지막 호출의 입력 토큰이 수십 만에 이른다.

해결은 두 축으로 갔다.

  • Hard budget: DMASS_MAX_BUDGET_USD 를 설정하면 누적 비용이 이걸 넘는 순간 LLM 호출이 throw된다. Agentic 루프도 95% 도달 시 자발적 종료.
  • 에이전트별 공급자 라우팅: recon 단계는 Haiku, 실제 취약점 진단은 Sonnet, reporter의 임원 브리핑만 Opus — 이런 식으로 환경변수 하나로 분리 가능하게 했다.

결과적으로 Juice Shop 풀스캔 한 번이 약 $2.18 에 들어온다. 이 정도면 CI/CD에 조용히 끼워 넣을 수 있는 수준이다.

3. False Positive의 독특한 양상

규칙 기반 스캐너의 FP와 LLM 기반 스캐너의 FP는 성질이 다르다. 규칙 기반은 "false match"가 많고, LLM 기반은 "그럴듯한 환각"이 많다. LLM이 "이건 SQL Injection일 가능성이 매우 높아 보인다"라고 자신만만하게 쓴 finding이 실제로는 재현이 안 된다.

이걸 잡으려고 Finding에 상태 라이프사이클을 박았다.

hypothesis → probable → confirmed → validated
                           ↓ (재현 실패 시)
                       false_positive

confirmed 가 붙으려면 LLM이 자기 툴로 재현에 성공해야 하고, validated 는 별도의 Validator 에이전트가 클린 세션에서 독립 재현에 성공해야 한다. Validator는 "finding을 발견한 에이전트의 말을 믿지 마라"는 전제로 동작하는 adversarial 역할이다. 시간 기반 finding인데 재현이 2초 미만이면 FP로 재분류하는 식의 규칙이 들어가 있다.

4. Blind 취약점은 OOB가 없으면 못 잡는다

Blind SQLi, Blind SSRF, Blind Command Injection 같은 건 응답 본문만 봐서는 절대 못 잡는다. 타겟 서버가 외부로 DNS 쿼리나 HTTP 요청을 날리는지를 관찰해야 확정된다.

그래서 OOB 서버를 내장했다. LLM이 issue_oob_token 을 호출하면 { token, dns_domain, http_url } 을 받고, 페이로드에 그 도메인을 삽입한다. 타겟이 nslookup abc123.oob.dmass.local 를 실행하면 DNS 쿼리가 DMASS의 DNS 리스너(Node.js dgram으로 RFC 1035 최소 구현)에 도착하고, LLM은 check_oob_callback 으로 이걸 확인해서 "blind 취약점 확정" 을 내린다.

물론 한계는 명확하다. 로컬 테스트에서는 공인 도메인/IP가 없으면 동작하지 않는다. 이건 지금도 해결 못 한 알려진 한계로 남아 있다.


그래서 성능은?

호기심에서 시작했으니 이 질문이 제일 중요하다. OWASP Juice Shop을 타겟으로 한 실제 블랙박스 스캔 결과를 그대로 옮긴다.

항목수치
실행 시간234.7초 (약 4분)
LLM 비용$2.18
입력 토큰660,403
출력 토큰13,261
총 Finding11
Critical3
High2
Medium4
Low2
LLM 제안 공격 체인3

발견된 카테고리 목록:

  • SQL_INJECTION — Product Search의 Error-based SQLi (confidence 0.95, confirmed)
  • BROKEN_FUNCTION_LEVEL_AUTHORIZATION — 인증 없이 접근되는 admin config / admin version 엔드포인트 (confidence 1.0, confirmed)
  • INFORMATION_DISCLOSURE — 인증 없는 security challenges 노출
  • SECURITY_HEADER_MISSING — HSTS / CSP / Referrer-Policy 누락
  • CORS_MISCONFIGURATION — wildcard CORS (hypothesis로 남김, confidence 0.6)
  • ATTACK_CHAIN — LLM 제안 체인 3건 (SQLi+Config / Recon→Admin Takeover / XSS+SQLi+CORS)

객관적으로 보면 "압도적이지는 않지만 진지하게 쓸 만한 수준" 이라는 것이 솔직한 평가다. Burp Suite Pro나 Acunetix 같은 상용 스캐너의 완성도에는 미치지 못한다. 하지만 혼자 만든 도구가 4분에 $2로 11개의 진짜 취약점과 3개의 체인 시나리오를 뱉는다는 결과를 두고 "성능이 쓸 만한가"라는 질문에는 자신 있게 그렇다고 답할 수 있다.

더 인상적이었던 건 정성적인 부분이었다. LLM이 제안한 체인 시나리오들은 결정론적 스캐너가 절대 쓸 수 없는 종류의 문장이었다. "SQL Injection으로 DB를 덤프한 뒤, 인증 없이 접근되는 admin config에서 얻은 정보를 조합하면 DB 자격증명 자체가 노출될 수 있다" 는 식의 내러티브는, 실제 펜테스터의 보고서 문장과 구분하기 어렵다.


아키텍처 정리

전체 구조를 한 장으로 옮기면 이렇다.

┌──────────────────── CLI ────────────────────┐
│ mode-router  scope-loader  profile-loader    │
│ workspace    scope-guard                     │
└──────────────────┬──────────────────────────┘
                   ▼
      ┌─────── Orchestrator ─────────┐
      │ 페이즈 상태머신                │
      │ TaskTree (우선순위)           │
      │ ContextManager (컨텍스트 건강도) │
      │ CheckpointManager             │
      └──┬──────────────────────┬─────┘
         ▼                      ▼
 ┌─ Shared Layer ─┐  ┌── LLM Providers ──┐
 │ RequestCoordin │  │ Anthropic         │
 │ LLMClient      │  │ OpenAI            │
 │ AgentToolkit   │  │ Bedrock           │
 │ AuthHandler    │  │ (tool-use 지원)   │
 │ InjectionProbe │  └───────────────────┘
 │ WAFEvasionLayer│  ┌── OOB Server ────┐
 │ WAFFeedbackLoop│  │ DNS Listener      │
 └────────────────┘  │ HTTP Listener     │
                     └───────────────────┘
         ┌──── 에이전트 ────┐
         │ RECON (static/dynamic)    │
         │ VULN (LLM-driven × 5)     │
         │   injection / auth / authz│
         │   / ssrf / xss            │
         │ VULN (결정론 × 4)          │
         │ ANALYSIS                  │
         │   chain-synthesizer       │
         │   metacog / validator     │
         │ OUTPUT (reporter)         │
         └───────────────────────────┘

페이즈 상태머신은 INIT → RECON → ANALYSIS → EXPLOITATION → VALIDATION → CHAIN_SYNTHESIS → REPORTING → DONE 로 흘러가고, 각 페이즈 완료 시 체크포인트를 원자적으로 저장한다. 중간에 예산 초과나 네트워크 차단으로 끊겨도 --resume 으로 이어서 실행된다. 이 설계 덕분에 "비용이 폭발해도 이미 만든 findings는 손실되지 않는다" 는 안정감이 생긴다.


배운 것들

혼자 만들며 체감한 원칙 몇 가지를 메모처럼 남긴다.

  1. LLM을 믿되 경계에서 강제하라. 스코프, 비용, mutation 허용 여부 같은 건 절대 LLM에게 맡기면 안 된다. Coordinator 계층에서 무조건 걸러야 한다.
  2. Finding의 라이프사이클을 설계하라. hypothesis / probable / confirmed / validated / false_positive 로 나눠 두면, LLM이 환각을 쓰더라도 하위 상태에 머물고 최종 보고서에서는 정리된다.
  3. 툴을 잘게 쪼개라. http_request 하나만 주면 LLM이 모든 걸 거기서 해결하려 들고 프롬프트가 부풀어오른다. timing_baseline, compare_responses 같은 전용 툴로 분리하면 토큰도 아끼고 논리도 깔끔해진다.
  4. 체크포인트는 초기부터 깔아라. Agentic 루프는 반드시 언젠가 도중에 죽는다. 죽었을 때 잃는 것이 없어야 계속 실험할 수 있다.
  5. 비용은 예산이 아니라 하드 리밋으로 관리하라. "$30 쓰려고 했는데 정신 차려보니 $80" 이 한 번만 나와도 심리적 진입장벽이 생긴다. 95% 도달 시 agentic 루프 자동 종료는 필수였다.

혼자 만든 도구는 어디까지 갈 수 있나

처음으로 돌아가 본다. 나만의 도구는 어느 정도의 성능을 가질 수 있을까.

결론은, "상용 도구를 대체할 수는 없지만, 상용 도구가 못 하는 일을 할 수 있다" 였다. DMASS는 Burp Suite Pro의 페이로드 커버리지를 따라잡을 수는 없다. 그러나 Burp Suite Pro는 "SQLi + Admin Config = Database Compromise" 라는 체인 시나리오를 한국어 임원 브리핑으로 써주지 않는다. LLM 네이티브 도구는 LLM이 잘하는 축에 집중하면 된다. 완성도의 절대치가 아니라, 가치의 축이 다른 도구를 만들어 내는 것.

AI 시대에 도구를 만든다는 건 바로 이 지점인 것 같다. "기존 카테고리의 1등이 되자" 가 아니라, "기존 카테고리에 없던 가치 축을 찾아서 맨 처음으로 그 축 위에 서자" 라는 쪽. 그 축 위에서는 혼자 만든 도구도 충분히 의미 있는 성능을 낸다.

DMASS는 지금도 계속 실험 중이다. AST 파서가 없어서 복잡한 taint flow를 못 잡고, OOB는 공인 도메인이 있어야 동작하고, 일부 고급 exploit은 Docker 샌드박스 의존이 있다. 한계는 README에 전부 적어 뒀다. 다만 "혼자 만든 도구가 어디까지 가는가"에 대한 첫 답은 이미 나왔다. 생각보다 꽤 멀리까지 간다.

호기심 하나로 시작했던 질문에, 스스로 만든 증거로 답한 것이 이 프로젝트에서 가장 좋았던 부분이었다.


소스: github.com/lufianlee/dmass
면책: 이 도구는 보안 연구·허가된 환경의 진단 목적으로만 사용해야 한다.

profile
이군의 보안, 그리고 생각을 다룹니다.

0개의 댓글