이 글에서는 OpenAI의 o3 모델을 이용해 Linux 커널의 ksmbd에서 원격 제로데이 취약점(CVE-2025-37899)을 찾은 과정을 상세히 다룹니다. 벤치마킹 및 기술적 분석을 통해 LLM이 실제 코드 취약점 탐지에서 보인 진보와 한계를 살펴봅니다.
이 글에서는 OpenAI의 o3 모델을 이용해 Linux 커널의 zeroday 취약점을 어떻게 발견했는지 소개합니다. 사용한 것은 오직 o3 API 뿐이었으며, 별도의 스캐폴딩, 에이전트 프레임워크 또는 추가 툴 없이 진행했습니다.
최근 저는 ksmbd의 취약점을 감사하고 있었습니다. ksmbd는 리눅스 커널 공간에서 SMB3 프로토콜을 구현하는 파일 공유 서버입니다. 원래 이 프로젝트는 LLM 관련 도구 개발에서 잠시 벗어나고자 시작했지만, o3가 출시되자 그 성능을 ksmbd에서 발견했던 버그로 빠르게 테스트해 보고 싶었습니다. 다음 글에서는 발견한 모든 버그에 대한 o3의 성능을 다룰 예정이고, 이번 글에서는 벤치마크 중 o3가 어떻게 제로데이 취약점(CVE-2025-37899, 패치)을 찾아냈는지 집중합니다. 이 취약점은 SMB 로그오프 명령의 핸들러에서 발생하는 use-after-free로, 여러 동시 세션이 객체를 특정 조건에서 공유하는 흐름을 이해해야 발견할 수 있습니다. o3는 이를 파악하고, 참조카운트 없이 운용되는 오브젝트가 한 스레드에 의해 해제되면서도 다른 스레드에는 여전히 노출되어 있는 상황을 정확히 지적했습니다. 제가 아는 한, 이런 유형의 취약점을 LLM이 처음으로 공개 석상에서 찾아낸 사례입니다.
기술적 세부사항에 들어가기 전에, 이 글의 핵심을 먼저 말씀드리자면 다음과 같습니다: o3의 등장으로 LLM은 코드 해석 및 분석 능력에서 큰 도약을 이루어냈으며, 취약점 리서치에 종사하는 분들은 반드시 주목해야 합니다. 베테랑 취약점 연구자나 익스플로잇 개발자를 당장 대체하지는 않지만, 이제는 인간을 훨씬 더 효율적이고 효과적으로 만들어 줄 단계에 왔다고 할 수 있습니다. 1만 줄 이하의 코드로 표현 가능한 문제라면 o3가 직접 해결하거나, 연구자가 해결하는 데 실질적인 도움을 줄 수 있다는 의미입니다.
먼저, 제가 수동으로 찾아냈고 o3의 벤치마크 대상으로 삼았던 CVE-2025-37778에 대해 살펴보겠습니다. 이 취약점은 kerberos 인증 경로에서 "session setup" 요청을 처리할 때 발생하는 use-after-free 버그입니다. 설명의 편의를 위해 "kerberos 인증 취약점"이라 부르겠습니다.
루트 원인은 다음과 같습니다:
cstatic int krb5_authenticate(struct ksmbd_work *work, struct smb2_sess_setup_req *req, struct smb2_sess_setup_rsp *rsp) { ... if (sess->state == SMB2_SESSION_VALID) ksmbd_free_user(sess->user); retval = ksmbd_krb5_authenticate(sess, in_blob, in_len, out_blob, &out_len); if (retval) { ksmbd_debug(SMB, "krb5 authentication failed\n"); return -EINVAL; } ... }
krb5_authenticate
함수가 세션 상태가 SMB2_SESSION_VALID
임을 감지하면 sess->user
를 해제합니다. 여기서의 가정은 이후에 ksmbd_krb5_authenticate
가 해당 포인터를 새로운 값으로 재초기화하거나, -EINVAL로 리턴할 경우 이후에 sess->user
를 참조하지 않는다는 것입니다. 그러나 이 가정은 깨집니다. ksmbd_krb5_authenticate
가 sess->user
를 재초기화하지 않는 경로로 유도할 수 있고, 심지어 -EINVAL 반환 이후에도 sess->user
에 접근이 가능합니다.
이 버그는 다음의 점에서 LLM 평가에 좋은 벤치마크입니다:
sess->state == SMB2_SESSION_VALID
를 달성하여 free를 유발하는 방법을 이해.ksmbd_krb5_authenticate
내에 sess->user를 재초기화하지 않는 경로가 있음을 파악하고 그 경로로 유도하는 논리 전개.이제 취약점 검출 테스트에 사용할 코드를 어떻게 LLM에게 제시할 것인지 고민해야 합니다. 저의 목적은 o3가 가상의 자동화 취약점 탐지 시스템의 백엔드로 동작할 때 성능을 평가하는 것입니다. 실제 사용시 자동화 시스템이 코드를 어떤 논리로 LLM에게 전달할지 명확해야만 현실적인 벤치마킹이 가능하기 때문입니다.
이상적으로는 전체 리포지토리 코드를 한 번에 주고 결과만 받으면 좋겠지만, 컨텍스트 윈도우 제한과 대량 입력시의 성능 저하로 인해 아직은 불가능합니다.
그래서 제가 생각한 대안은 각각의 SMB 커맨드 핸들러 함수별로 해당 핸들러 및 경로 내 호출되는 함수들(깊이 3까지)을 모두 묶어서 LLM에게 주는 방식입니다. 더불어 네트워크에서 데이터를 읽어 파싱, 커맨드 핸들러 선택, 처리 종료 후 연결 종료까지의 함수도 전부 포함시켜야 LLM이 구조체 등 데이터가 언제, 어떻게 생성되는지 정확히 추론할 수 있습니다. 이렇게 하면 총 3.3k LoC(약 27k 토큰)가 되어, 이전 모델들과 o3를 비교하는 벤치마크로 사용하기 적합해집니다. 분석에 사용한 코드는 여기에 있습니다.
프롬프트 구성도 중요합니다. 사용한 시스템 프롬프트와 기타 입력 정보는 이 Github 저장소의 .prompt 파일에서 확인할 수 있습니다. 주요 내용은 다음과 같습니다:
실행에는 llm 도구를 사용해 다음과 같이 쿼리를 날렸습니다:
$ llm --sf system_prompt_uafs.prompt \
-f session_setup_code.prompt \
-f ksmbd_explainer.prompt \
-f session_setup_context_explainer.prompt \
-f audit_request.prompt
이 실험을 N=100번 반복했으며, 결과를 저장했습니다(코드 파일을 재생성하면서 미세한 차이가 있을 수는 있습니다). 결과적으로 o3는 100회 중 8번 kerberos 인증 취약점을 탐지했고, 66번은 문제가 없다고 판단(거짓 음성), 나머지 28번은 거짓 양성 보고였습니다. 비교를 위해 Claude Sonnet 3.7은 3/100, Sonnet 3.5는 0/100을 기록했습니다. 최소한 이 벤치마크에서 o3는 Sonnet 3.7 대비 2~3배의 개선을 보였습니다.
흥미 있으신 분은 o3 결과 리포트 샘플(여기)와 Sonnet 3.7 결과(여기)도 참고하실 수 있습니다. o3는 결과를 간결하게 요점만 잘 짚은 사람 작성 레포트처럼 내보내는 반면, Sonnet은 일종의 작업 로그나 생각 흐름처럼 결과를 보여줍니다. 각 방식에 장단점이 있지만, o3의 구조화되고 집중된 답변은 따라가기 쉽다는 장점이 있습니다.
o3가 session setup 핸들러 코드만 주면 kerberos 인증 취약점을 잡을 수 있음을 확인하고, 이번에는 모든 커맨드 핸들러 코드를 한 번에 주면 어떨지 실험했습니다. 이는 훨씬 더 어려운 문제입니다. 핸들러 전체는 smb2pdu.c에 있고, 약 9천 줄에 달합니다. 전체 핸들러와 연결 관련 코드까지 포함하면 12k LoC(약 10만 토큰)가 넘습니다.
이렇게 전체 코드를 입력해 100회 실험한 결과, kerberos 인증 취약점은 1회만 탐지되었습니다. 확실한 성능 저하이지만, 여전히 가능성은 남아 있습니다. 하지만 더 눈에 띄는 점은 다른 결과에서 제가 기존에 모르던 유사하지만 새로운 취약점이 발견된 것입니다. 이번에는 사용자 session logoff 핸들러에서의 free였습니다.
cint smb2_session_logoff(struct ksmbd_work *work) { struct ksmbd_conn *conn = work->conn; struct ksmbd_session *sess = work->sess; ... ksmbd_close_session_fds(work); ksmbd_conn_wait_idle(conn); ... down_write(&conn->session_lock); sess->state = SMB2_SESSION_EXPIRED; up_write(&conn->session_lock); if (sess->user) { ksmbd_free_user(sess->user); sess->user = NULL; } ... }
o3의 요약을 거의 그대로 옮기면 다음과 같습니다:
ksmbd 워커 스레드 하나가 여전히 sess->user를 사용하는 동안, SMB2 LOGOFF를 처리하는 또 다른 스레드가 같은 세션의 struct를 해제합니다. 포인터를 보호하는 동기화가 없어 첫 번째 스레드는 해제된 메모리를 참조(use-after-free)하며, 이는 커널 메모리 손상, 임의 코드 실행으로 이어집니다.
이 취약점을 읽으며, AI 도구가 버그헌팅에서 얼마나 도움이 될 수 있는지에 대한 제 인식이 크게 변했습니다. 설령 o3 수준에서 더 이상 발전하지 않는다고 해도, 버그헌터라면 어떤 워크플로우에 적용할지 고민하고, 실제 활용할 도구로 접목하는 것이 타당합니다. 물론 o3의 신호대잡음비가 약 1:50 정도임은 문제지만, 이는 점차 해결중인 과제라 볼 수 있습니다.
또한 kerberos 인증 취약점을 찾았을 때 내놓은 패치는 다음과 같았습니다:
diffdiff --git a/fs/smb/server/smb2pdu.c b/fs/smb/server/smb2pdu.c index d24d95d15d87..57839f9708bb 100644 --- a/fs/smb/server/smb2pdu.c +++ b/fs/smb/server/smb2pdu.c @@ -1602,8 +1602,10 @@ static int krb5_authenticate(struct ksmbd_work *work, if (prev_sess_id && prev_sess_id != sess->id) destroy_previous_session(conn, sess->user, prev_sess_id); - if (sess->state == SMB2_SESSION_VALID) - ksmbd_free_user(sess->user); + if (sess->state == SMB2_SESSION_VALID) { + ksmbd_free_user(sess->user); + sess->user = NULL; + } ...
그런데 o3 리포트를 읽으며, 이 패치는 불충분하다는 사실을 알게 되었습니다. 실제로 logoff 핸들러는 이미 sess->user = NULL을 실행하지만, SMB는 여러 연결이 동일 세션에 바인딩할 수 있기에 free 이후 NULL 세팅 전의 짧은 틈에 다른 쓰레드가 sess->user를 여전히 사용할 수 있습니다. 알고도 놓친 허점이었죠.
o3가 kerberos 인증 취약점 검색에서 내놓은 여러 리포트 중 일부는 저와 마찬가지로 이 점을 간과했지만, 다른 리포트에선 session binding이 허용하는 위험까지 인지하여 단순 NULL 대입으로는 결코 완전 해결이 아님을 지적했습니다. 만약 o3를 버그 탐지 및 수정 보조로 활용했다면 더 질 높은 결과를 얻었을 지도 모릅니다. 물론 현재로선 오탐 비율이 높아 모든 리포트를 꼼꼼히 검토하기엔 쉽지 않지만, 앞으로는 신호비가 계속 개선될 것입니다.
LLM은 프로그램 분석 기술 중 인간과 가장 가까운 위치에 도달했습니다. 창의성, 유연성, 범용성 측면에서 LLM은 전통적 symbolic execution, abstract interpretation, fuzzer보다 훨씬 사람 분석가에 가깝습니다. GPT-4부터 가능성은 감지됐지만, 실제 문제에선 기대만큼 실적이 없었습니다. 하지만 o3는 충분히 실용적으로 코드를 분석하고, 문제를 해결하며 인간 연구자를 확실히 보조해줄 수준에 도달했습니다.
o3가 완벽하지는 않습니다. 여전히 이해불가한 결과를 내놓거나 실망시킬 확률이 높습니다. 그러나 이제는 처음으로, 올바른 결과가 나올 확률이 충분히 높아져 실제 문제에 적용할 가치가 생겼습니다.