Slice: SAST + LLM 인터프로시저(함수 간) 컨텍스트 추출기

ko생성일: 2025. 8. 21.갱신일: 2025. 8. 26.

CodeQL, Tree-Sitter, 그리고 LLM을 결합한 Slice로 복잡한 호출 그래프 전반에서 UAF를 찾는 방법을 소개하고, CVE-2025-37778 재현 과정을 통해 선택·필터링·순위화 워크플로를 보여줍니다.

올여름 초, Sean Heelan이 o3를 활용해 리눅스 커널에서 use-after-free 취약점을 찾은 과정을 상세히 담은 훌륭한 블로그 글을 발표했습니다. 인터넷은 큰 반향을 보였고, 그럴 만한 이유가 있습니다. 2022년 말 ChatGPT가 처음 공개된 이후로 우리 모두가 궁금해했던 질문이 있었죠. LLM이 실제로 널리 사용되는 프로덕션 코드베이스에서 복잡한 취약점을 찾아낼 수 있을까? 리눅스 커널은 이 질문에 답하기에 훌륭한 연구 대상입니다.

아직 Sean의 글을 안 읽었다면, 꼭 읽어 보세요. 제 입장에서는 다음과 같은 향후 과제들이 특히 눈에 띄었습니다.

  • 어떤 잠재적으로 취약한 코드를 LLM에 보낼지 어떻게 결정할 것인가?
  • 자동화된 시스템이 어떻게 해당 함수를 선택할지 명확히 설명할 수 없다면, 임의로 함수를 골라 LLM에 보여 주는 것은 소용이 없습니다. 이상적인 LLM 활용은 리포지토리의 모든 코드를 주면, 그것을 섭취하고 결과를 뱉어내는 것입니다. 하지만 컨텍스트 윈도우 한계와 컨텍스트가 늘어날수록 성능이 회귀하는 문제 때문에 지금 당장 실용적으로 가능하진 않습니다.
  • 탐지 범위를 확장하면서도 신호 대 잡음비를 높게 유지하고 거짓 양성을 어떻게 줄일 것인가?
  • 벤치마크에서 o3는 kerberos 인증 취약점을 100회 중 8회 찾아냅니다. 다른 66회에서는 코드에 버그가 없다고 결론 내리며(거짓 음성), 나머지 28건은 거짓 양성입니다. 비교하면, Claude Sonnet 3.7은 100회 중 3회, Claude Sonnet 3.5는 100회 모두에서 찾지 못했습니다. 적어도 이 벤치마크에서는 o3가 Claude Sonnet 3.7 대비 2~3배 개선되었습니다.
    • 입력 토큰이 더 많아지면 o3는 100회 중 1회만 취약점을 찾아 성능 저하가 뚜렷하지만, 그래도 찾기는 합니다.
  • 오늘날의 모델을 연구자의 워크플로에 어떻게 현실적으로 녹여 넣을 것인가?
  • 만약 우리가 지금의 o3 수준에서 더 진전이 없더라도, VR에 종사하는 모든 사람은 무엇이 워크플로에서 이득을 줄 수 있는지 파악하고, 그것을 연결하는 툴링을 구축하는 것이 여전히 합리적입니다.

TL;DR: 저는 이 질문들에 답하기 위해 Slice(SAST + LLM Interprocedural Context Extractor)를 만들었습니다. CVE-2025-37778 발견을 재현하는 과정을 통해 Slice가 어떻게 작동하는지 보여 드리겠습니다.

$ slice -h
Slice: SAST + LLM Interprocedural Context Extractor
CodeQL, Tree-Sitter, 그리고 LLM을 사용해 복잡한 호출 그래프 전반에서 취약점을 찾습니다.
의도된 흐름은 query -> filter -> rank 입니다.

Available Commands:
  parse       코드 파싱 및 함수 정보 추출
  query       CodeQL 쿼리 실행 및 소스 코드로 결과 보강
  filter      LLM 처리를 통해 CodeQL 취약점 결과를 필터링
  rank        검증된 취약점 결과를 중요도로 순위화

재현: CVE-2025-37778

앞서 언급한 use-after-free를 AI 가속 도구로 찾아보고자 했습니다.

  • 여러 번 반복 실행해도 가능한 한 일관되게
  • 코드베이스에 대한 사전 지식 없이
  • 빌드/컴파일 없이(그냥 코드를 읽기만 해서!)

원 실험과 유사한 제약을 따르면서요.

  • 명시적으로 use-after-free를 탐지
  • 호출 그래프 분석 깊이 3
  • MCP 의미의 툴 사용이나 에이전트 프레임워크 없음

위 질문들을 하나씩 살펴보겠습니다.

어떤 코드를 분석할까?

시작은 단순합니다. 우리가 use-after-free를 찾으려는 거라면, free된 객체가 나중에 사용되는 경우를 찾으면 됩니다. 쉬워 보이죠? free류 호출을 찾는 건 .*free.* 정규식으로 간단하지만, free된 객체를 추적하는 일(특히 함수 경계를 넘는 경우)은 까다로울 수 있습니다. 여러 정적 분석 도구의 taint 추적 기능을 시험해 봤지만, 대부분은 어느 정도의 컴파일을 요구했습니다. 저는 이 요구사항을 좋아하지 않습니다. 대규모 저장소에서 개별 파일이나 코드 스니펫(혹은 약간 깨진 코드)에도 확장해 분석하기 어려워지기 때문입니다. 그래서 선택지가 좁았습니다.

행운처럼—제가 프로젝트를 진행하던 중 GitHub이 C/C++ 리포지토리에 대해 빌드 없는 스캐닝으로 CodeQL을 대규모로 사용할 수 있게 되었다고 발표했습니다! 이는 매우 유용하지만 몇 가지 제약이 뒤따릅니다. 예컨대 실제로 프로젝트를 빌드하지 않기 때문에, #ifdef로 가드된 조건부 코드 블록을 포함시키기 위해 전처리기 매크로를 지정할 수 없다고1 (적어도 제 지식으로는) 합니다. 저는 단순히 해당 지시어를 #ifndef로 뒤집어 코드베이스의 더 많은 부분을 실행하도록 우회했습니다.

$ find fs/smb/server/ -name "*.c" -o -name "*.h" |
    xargs sed -i 's/^#ifdef/#ifndef/g'

이제 CodeQL 데이터베이스를 만들고 쿼리를 실행할 수 있습니다. 저는 Linux v6.15-rc3/fs/smb/server/를 대상으로 지정하고, 기본 제공 쿼리인 UseAfterFree.qlUseAfterExpiredLifetime.ql을 테스트했지만 성과가 없었습니다. 이는 크게 놀랍지 않았습니다. 많은 기본 정적 분석 규칙은 자동화된 CI/CD 파이프라인에서 과도한 노이즈를 내지 않도록 거짓 양성을 낮추는 데 최적화되어 있기 때문입니다. 저는 CodeQL이 처음이라 쿼리를 직접 작성해 함수 간 UAF를 넓게 잡아 보려고 했습니다. 예상대로, 노이즈를 줄이려는 시도에도 불구하고 거짓 양성이 아주 많이 나왔습니다.

$ codeql database create smb-server \
    --language=cpp \
    --source-root=fs/smb/server/ \
    --build-mode=none  # 중요!

# 쿼리에서 cpp 관련 항목을 쓰기 위해
$ cat > qlpack.yml << EOF
name: uaf
dependencies:
  codeql/cpp-all: "*"
EOF
$ codeql pack install

$ codeql query run uaf.ql \
    --database=smb-server \
    --output uaf.bqrs

# 우리가 찾으려는 버그
$ codeql bqrs decode --format=csv uaf.bqrs |
    xsv search -s free_func krb5_authenticate |
    xsv search -s use_func smb2_sess_setup |
    xsv flatten
usePoint          user
object            user
free_func         krb5_authenticate
free_file         smb2pdu.c
free_func_def_ln  1580
free_ln           1606
use_func          smb2_sess_setup
use_file          smb2pdu.c
use_func_def_ln   1668
use_ln            1910

# 헉… 다행히 이 글의 주제가 AI를 이용한 트리아지네요!
$ codeql bqrs decode --format=csv uaf.bqrs | xsv count
1722

좋습니다. 매우 관대한 CodeQL 쿼리에 따르면 1700건이 넘는 잠재적 UAF가 있습니다—이제 무엇을 할까요? CodeQL 결과는 주로 소스 코드의 라인 번호와 심볼 이름 같은 일부 정보만 제공하므로, Tree-sitter를 사용해 LLM에 전달하고 싶은 나머지 코드 컨텍스트를 가져올 수 있습니다. 또한 Tree-Sitter로 코드베이스를 파싱하면 결과를 제한할 호출 깊이를 제어할 수 있습니다.

$ slice query -h
CodeQL 쿼리를 데이터베이스에 실행하고 Tree-Sitter 파싱을 사용해 취약점
발견 결과를 전체 소스 코드 컨텍스트로 보강합니다.

Flags:
  -c, --call-depth int      최대 호출 체인 깊이(-1 = 무제한)
  -b, --codeql-bin string   CodeQL CLI 바이너리 경로
  -j, --concurrency int     결과 처리를 위한 동시 워커 개수
  -d, --database string     CodeQL 데이터베이스 경로(필수)
  -q, --query string        CodeQL 쿼리 파일(.ql) 경로(필수)
  -s, --source string       소스 코드 디렉토리 경로(필수)

아래의 slice query 서브커맨드는 이미 생성된 CodeQL 데이터베이스에 쿼리를 실행하고, Tree-Sitter로 결과를 보강한 뒤, 지정한 최대 호출 깊이를 만족하는 결과만 반환합니다.

$ slice query \
    --call-depth 3 \
    --database smb-server \
    --query uaf.ql \
    --source fs/smb/server/ \
    >query.json

$ jq '.results | map(select(.query |
        .free_func == "krb5_authenticate"
        and
        .use_func == "smb2_sess_setup"
    ))[0]' query.json |
	cut -c -80
{
  "query": {
    "object": "user",
    "free_func": "krb5_authenticate",
    "free_file": "smb2pdu.c",
    "free_func_def_ln": 1580,
    "free_ln": 1606,
    "use_func": "smb2_sess_setup",
    "use_file": "smb2pdu.c",
    "use_func_def_ln": 1668,
    "use_ln": 1910
  },
  "source": {
    "free_func": {
      "def": " 1580  static int krb5_authenticate(struct ksmbd_work *work,\n 158",
      "snippet": "ksmbd_free_user(sess->user);"
    },
    "use_func": {
      "def": " 1668  int smb2_sess_setup(struct ksmbd_work *work)\n 1669  {\n 16",
      "snippet": "if (sess->user && sess->user->flags & KSMBD_USER_FLAG_DELAY_SE"
    },
    "inter_funcs": []
  },
  "calls": {
    "valid": true,
    "reason": "Target function can reach source function",
    "chains": [
      [
        "smb2_sess_setup",
        "krb5_authenticate"
      ]
    ],
    "details": "Reverse call: smb2_sess_setup calls krb5_authenticate",
    "min_depth": 1,
    "max_depth": 1
  }
}

$ jq '.results | length' query.json
217

1722건보다는 217건이 훨씬 그럴듯하지만, 여전히 많습니다. 이제 UAF 후보를 찾고 그 결과를 LLM용 추가 컨텍스트로 보강하는 강력한 방법을 갖췄습니다. 계속 가봅시다.

신호 대 잡음비를 어떻게 끌어올릴까?

217개의 잠재적 use-after-free를 수동으로 트리아지하는 대신, LLM으로 두 번의 패스를 진행합니다.

  1. 비교적 작은 모델로 트리아지. 보안 함의는 잠시 제쳐두고—“use”가 정말로 “free” 이후에 도달 가능한가? 정말 같은 객체를 대상으로 동작하는가? 이는 CodeQL 결과를 상식적으로 점검2하고 다음 단계에서 토큰을 낭비하지 않기 위한 빠른 검사입니다.
  2. 큰 모델로 문법적으로 유효한 UAF를 더 깊이 분석. 어떤 조건에서 실제로 해제된 메모리에 접근이 일어나는가? 실제로 익스플로잇 가능한가? 어떻게 고칠 수 있는가?

두 단계 모두 slice filter 서브커맨드를 사용할 수 있습니다.

$ slice filter -h
LLM을 사용해 CodeQL 취약점 탐지 결과를 필터링합니다.

이 명령은 템플릿 기반 접근으로 결과를 처리합니다.
템플릿은 동작, 출력 구조, 처리 파라미터를 결정합니다.

Flags:
  -a, --all                       유효성 여부와 관계없이 모든 결과 출력
  -b, --base-url string           OpenAI 호환 API의 베이스 URL
  -j, --concurrency int           동시 LLM API 호출 개수
  -t, --max-tokens int            응답의 최대 토큰 수
  -m, --model string              사용할 모델
  -p, --prompt-template string    커스텀 프롬프트 템플릿 파일 경로
  -r, --reasoning-effort string   GPT-5 모델의 reasoning effort
      --temperature float32       응답 생성 온도
      --timeout int               타임아웃(초)

아래 파이프라인은 CodeQL 쿼리 출력을 두 번의 filter 명령으로 넘기며, .results 배열의 각 원소에 .triage.analyze 키를 추가합니다. 트리아지에는 빠르고 비용 효율적인 GPT-5 mini를, 분석에는 플래그십 GPT-5를 사용합니다(둘 다 높은 reasoning effort).

$ {
	slice query \
		--call-depth 3 \
		--database smb-server \
		--query uaf.ql \
		--source fs/smb/server/ |
		slice filter \
			--prompt-template triage.tmpl \
			--model gpt-5-mini \
			--reasoning-effort high \
			--concurrency 100 |
		tee triage.json |
		slice filter \
			--prompt-template analyze.tmpl \
			--model gpt-5 \
			--reasoning-effort high \
			--concurrency 20 \
			>analyze.json
} 2>&1 | grep 'token usage statistics' | grep -oE 'call.*'

calls=217 prompt_tok=394621 comp_tok=425909 reason_tok=398464 cost_usd=1.75
calls=9   prompt_tok=31425  comp_tok=90766  reason_tok=69184  cost_usd=1.64

$ for step in triage analyze; do
	echo -n "$step results: "
	jq '.results | length' "$step.json"
done

triage results:  9
analyze results: 1

실행 시간은 약 7분, 비용은 $1.75(트리아지) + $1.64(분석) = 총 $3.39. 결과 개수도 기대에 부합합니다. 살펴보죠.

$ jq '.results | map(.triage.reasoning)' triage.json | cut -c -80
[
  "Yes. ksmbd_free_user(user) is called earlier in ntlm_authenticate() (e.g. lin
  "Yes — on the path where sess->state == SMB2_SESSION_VALID the function call
  "Yes. ntlm_authenticate calls ksmbd_free_user(user) inside the sess->state ==
  "ntlm_authenticate() calls ksmbd_free_user(user). In some paths (e.g. sess->st
  "Yes. smb2_sess_setup calls krb5_authenticate; inside krb5_authenticate it unc
  "Yes — ntlm_authenticate() may call ksmbd_free_user(user) before returning t
  "Yes. ntlm_authenticate (called from smb2_sess_setup) can call ksmbd_free_user
  "Yes — based on the provided call chain there is a feasible path: smb_direct
  "Yes — per the provided call chain, smb_direct_post_recv_credits frees the p
]

$ jq '.results | map(.analyze | {summary, vuln})' analyze.json | fold
[
  {
    "summary": "Valid use-after-free: On Kerberos re-authentication failure, smb
2_sess_setup() dereferences sess->user->flags after krb5_authenticate() has free
d the previous sess->user without nulling it, leading to a UAF read in the error
 path.",
    "vuln": {
      "affected_object": "sess->user (struct ksmbd_user *)",
      "free_loc": {
        "file": "smb2pdu.c",
        "func": "krb5_authenticate",
        "line": 1606,
        "snippet": "if (sess->state == SMB2_SESSION_VALID)\n\tksmbd_free_user(se
ss->user);"
      },
      "type": "use-after-free (read)",
      "use_loc": {
        "file": "smb2pdu.c",
        "func": "smb2_sess_setup",
        "line": 1910,
        "snippet": "if (sess->user && sess->user->flags & KSMBD_USER_FLAG_DELAY_
SESSION)\n\ttry_delay = true;"
      }
    }
  }
]

좋습니다. 우리가 보고 싶었던 바로 그 버그입니다!

그렇다면, 일관적인가요?

그렇습니다. 연속 10회 실행3 모두에서 올바른 버그를 매번 찾아냅니다.

단계모델API 호출프롬프트 토큰완료 토큰추론 토큰비용
TriageGPT-5 mini217394,621417,573.8390,067.2$1.71
AnalyzeGPT-58.629,072.790,052.869,708.8$1.63
합계225.6423,693.7507,626.6459,776$3.35

그 10회 중 1회에서는 아래와 같은 추가 분석 결과도 반환되었습니다. 아직 검토하진 않았지만 일단 여기 문서화해 둡니다.

$ jq '.results[1] | .analyze | {summary, vuln}' analyze.json | fold
{
  "summary": "Likely use-after-free of struct smb_direct_transport (t) and/or it
s sendmsg mempool due to waking teardown on send_pending==0 before freeing send
messages, allowing teardown to free t->sendmsg_mempool while send_done still cal
ls smb_direct_free_sendmsg(), which dereferences t.",
  "vuln": {
    "affected_object": "struct smb_direct_transport *t (specifically t->sendmsg_
mempool and t->cm_id->device)",
    "free_loc": {
      "file": "transport_rdma.c",
      "func": "smb_direct_destroy_transport (teardown path)",
      "line": 0,
      "snippet": "mempool_destroy(t->sendmsg_mempool);\nkfree(t);"
    },
    "type": "use-after-free",
    "use_loc": {
      "file": "transport_rdma.c",
      "func": "smb_direct_free_sendmsg",
      "line": 489,
      "snippet": "mempool_free(msg, t->sendmsg_mempool);"
    }
  }
}

향후 과제

  • 모델 비교. GPT-5 패밀리를 Claude, Gemini, Grok 등과 비교하고, Qwen3, GLM-4.5, DeepSeek, gpt-oss 같은 오픈 가중치 모델과도 비교 측정.
  • 다른 언어 지원. CodeQL과 Tree-Sitter는 Go, Python, JavaScript 등 많은 컴파일/인터프리터 언어를 견고히 지원.
  • 대규모 코드베이스 분석. 이 실험은 리눅스 커널 내 사전 지정된 타깃으로 시작했지만, 대규모 코드베이스의 독립 구성요소를 자동 요약하고 높은 가치의 연구 타깃을 식별할 수 있다면 매우 강력할 것.
  • 분석 결과 순위화. 제가 다른 글에서 광범위하게 탐구한 개념으로, 이 문제에도 큰 적용 가능성이 있습니다. 서브커맨드는 이미 구현되어 있으나, 최종 결과 수가 아직은 적어 순위를 매길 만큼 충분하지 않았습니다.
  • 더 많은 취약점 클래스 추가. 새로운 CodeQL 쿼리와 프롬프트 템플릿만 있으면 됩니다.
  • 평가 데이터셋 확장. 지난 몇 년간의 고프로파일 취약점 몇 가지를 재현.
  • SARIF 출력. 정적 분석 도구의 표준.
  • 디컴파일된 코드 실험. Binary Ninja의 HIL은 LLM 분석에 좋습니다. 빌드 없는 CodeQL을 응용해 분석할 수 있는지에 관심이 있습니다. 초기 테스트에 도움 준 @SinSinology 감사합니다.
  • 동적 쿼리 생성. LLM을 사용해 즉석에서 CodeQL 쿼리를 생성. 아이디어 제공한 @evilsocket 감사합니다.

  1. 이 문제를 생각보다 오래 씨름했습니다.↩︎

  2. 물론 CodeQL 쿼리를 더 잘 쓰게 되는 것도 큰 도움이 되겠죠.↩︎

  3. 얘들아, 내가 돈이 무한히 많은 줄 아니?↩︎