메뉴
HN
Hacker News 11일 전

AI 감사 도구, OpenVM ZkVM서 심각한 취약점 발견

IMP
8/10
핵심 요약

AI 기반 보안 감사 도구인 'zkao'가 OpenVM의 zkVM 게스트 라이브러리에서 악의적인 증명자가 페어링 검증을 우회할 수 있는 치명적인 취약점(CVE-2026-46669)을 발견했습니다. 단순한 LLM 방식으로는 복잡한 모듈 간 상호작용을 분석하기 어렵기 때문에, 전문가의 감사 기법을 반영한 특화된 컨텍스트 엔지니어링 파이프라인을 구축한 것이 이번 성공의 핵심이었습니다. 해당 버그는 1.6.0 버전에서 수정 완료되었으며, AI가 단순 버그 탐지를 넘어 복잡한 암호학 코드의 구조적 취약점까지 찾아낼 수 있음을 증명한 중요한 사례입니다.

번역된 본문

이 글은 시리즈의 두 번째 포스트입니다. Cloudflare의 CIRCL에 대해 다룬 첫 번째 글을 읽지 않으셨다면, 우리가 왜 이 실험을 진행하는지와 파이프라인을 어떻게 구축했는지에 대한 배경이 나와 있으니 참고하시기 바랍니다. 이번 글에서는 우리의 AI 보안 감사 도구인 zkao를 OpenVM의 zkVM에 투입했고, 오픈소스 게스트 라이브러리인 openvm-pairing에서 악의적인 증명자가 임의의 페어링 동등성을 위조할 수 있는 치명적인 건전성(Soundness) 버그를 발견했습니다. 이는 zkVM 증명 시스템 자체의 버그가 아니며, 취약한 라이브러리를 사용하는 코드에만 영향을 미친다는 점에 유의해야 합니다. 이번 글의 버그에는 CVE-2026-46669 번호가 부여되었으며 OpenVM 1.6.0에서 수정되었습니다. 우리가 아는 한, OpenVM 기반으로 구축 중인 모든 파트너는 해당 버전으로 업그레이드를 완료한 상태입니다. 첫 번째 글과 마찬가지로 참고 사항을 명시합니다: AI는 최종 보고서가 아닌 '후보 발견물(candidate finding)'을 생성했습니다. 우리 팀의 인간 전문가들이 해당 문제를 검증하고, 익스플로잇(악용) 가능성을 확인하며, 전체적인 영향 범위와 피해를 입은 프로젝트를 파악하고 공개 및 수정 절차를 진행했습니다. 이번 사례의 경우, zkao가 스스로 만들어낸 상세한 보고서와 최소 개념 증명(PoC) 덕분에 매우 빠른 수동 검토만으로도 OpenVM 팀에 공유할 가치가 있다고 판단하기에 충분했습니다. 어떻게 발견되었는가 4개월 전, 우리는 다른 모든 대상을 처음 스캔할 때와 마찬가지로 AI 실험의 일환으로 OpenVM을 스캔했습니다. 단순한 프롬프트가 주어진 LLM을 사용했고, 이어서 우리 전문가들이 관리하는 스킬(기법)이 적용된 LLM을 사용했습니다. Opus 4.6과 Codex 5.3을 사용하여 실행했고, Opus 4.7과 Codex 5.4가 출시되자마자 다시 실행했습니다. 후보 발견물들은 모두 타당한 관찰이었으며, 모델들도 그중 몇 가지를 자신 있게 '치명적(Critical)' 또는 '높음(High)'으로 분류했지만 실제로는 악용 가능한 것이 하나도 없었습니다. 우리의 가설은 zkVM이 단순한 LLM 설정으로 처리하기에는 너무 복잡하며, 컨텍스트 창이 30만 토큰이나 100만 토큰이라 해도 마찬가지라는 것이었습니다. 모듈 간의 의존성은 일반적인 라이브러리보다 훨씬 조밀합니다. 암호학 라이브러리의 경우 각 서브 에이전트에게 단일 암호학 기본 요소에 해당하는 폴더를 할당하여 병렬로 감사할 수 있는 경우가 많습니다. 각 서브 에이전트는 소수의 코드 줄을 읽고 관련 스킬만 적용한 뒤, 발견한 내용을 마크다운 파일로 작성하며, 메인 에이전트가 이 파일들을 하나로 모읍니다. 이 모든 과정은 Claude Code나 Codex 같은 인기 있는 에이전트 코딩 도구에서 인간의 개입을 최소화하여 즉시 처리할 수 있습니다. 하지만 OpenVM과 같이 더 복잡한 코드베이스에서는 이 접근법이 통하지 않습니다. 쉽게 발견할 수 있는 사소한 버그들을 제외하면, 서브 에이전트의 유용한 출력 결과는 단순한 버그 목록이 아닙니다. 수학적으로 안전하다고 증명된 모듈 A와 모듈 B를 합쳤을 때 전체 시스템이 안전하지 않을 수 있습니다. 따라서 이렇게 모듈을 '분리'하는 방식으로 버그를 찾으면 의미 있는 버그를 잡을 수 없습니다. 대신 서브 에이전트의 출력은 모듈에 대한 '지식'이어야 합니다. 즉, 그 모듈이 어떤 것을 가정하고, 호출자에게 무엇을 위임하며, 어떤 불변값을 조용히 의존하고 있는지에 대한 정보여야 합니다. 하지만 이러한 종류의 출력을 잘 표현하는 것이 어려운 부분입니다. 너무 짧으면 실제 버그가 숨어 있는 구현 세부 사항을 건너뛰게 되고, 너무 길면 다른 결과와 결합되기도 전에 메인 에이전트의 컨텍스트를 초과하게 됩니다. 우리가 관찰한 바로는, 글을 작성하는 현 시점 기준으로 앞서 언급한 에이전트 코딩 도구들로는 이 문제를 효율적으로 해결하지 못합니다. 이러한 가설을 염두에 둔 채, 우리는 순수 LLM이 실제 버그를 먼저 발견한 이후에만 zkao를 실행한다는 원래 실험 규칙을 깨고 OpenVM에 zkao를 실행하기로 결정했습니다. 우리는 zkao를 위한 컨텍스트 엔지니어링에 많은 시간을 투자했고, 우리 자체 전문가들의 작업 방식을 취약점 발견용 재사용 가능한 워크플로우로 구워 넣었기 때문에, 이런 상황에 딱 맞는 적합한 도구처럼 보였습니다. 9시간 30분 이상의 스캔 끝에 zkao는 많은 결과를 반환했습니다. 이전 실험과 마찬가지로 우리는 모든 결과를 깊이 검토할 시간이 없었습니다. 빠르게 훑어보던 중 하나가 즉각적으로 눈에 띄었습니다. 바로 게스트 라이브러리 중 하나의 페어링 검사에서 발견된 치명적인 건전성 버그였습니다. 우리의 가설이 맞았고, 수개월의 노력이 결실을 맺은 것입니다! 비록 공유할 버그는 하나뿐이지만, 첫 번째 글과의 일관성을 유지하기 위해 여기에 해당 버그를...

원문 보기
원문 보기 (영어)
This is the second post in the series. In case you have not read the first one on Cloudflare's CIRCL , it has more context on why we run these experiments and how our pipeline is set up. In this post, we pointed zkao , our AI auditor, at OpenVM's zkVM , and it found a critical soundness bug in its guest library openvm-pairing that lets a malicious prover forge any pairing equality. Note that this is not a soundness bug in the zkVM's proving system itself; it only affects code that uses the vulnerable library. The bug in this post was assigned CVE-2026-46669 and fixed in OpenVM 1.6.0. As far as we know, all partners building on OpenVM have since upgraded to that version. Note Clarification , same as in the first post: the AI produced a candidate finding, not a final report. Humans on our team then validated the issue, confirmed exploitability, understood the full impact and affected projects, and handled disclosure. In this case, a very quick manual triage was enough to decide it was worth sharing with the OpenVM team, thanks to the detailed report and a minimal PoC that zkao produced itself. How it happened Four months ago, we scanned OpenVM as part of our AI experiment, the same way we scan everything at first: an LLM with a simple prompt, and then an LLM with our expert-maintained skills. We ran it with Opus 4.6 and Codex 5.3. As soon as Opus 4.7 and Codex 5.4 came out, we ran them again. The candidate findings were all valid observations, and the models confidently labeled several of them as Critical or High, but none of them were actually exploitable. Our hypothesis was that a zkVM is simply too complex for a naive LLM setup to handle with 300K tokens or even 1M tokens of context. The dependencies between modules are far denser than in a typical library. A cryptography library can often be audited in parallel by simply handing each subagent a folder that maps to a single cryptographic primitive. Each subagent reads a small number of lines, applies only the relevant skills, writes its findings to a markdown file, and the main agent stitches those files together. All of this happens out of the box with popular agentic coding tools such as Claude Code and Codex, with little human steering. That approach does not transfer to a more complex codebase, like OpenVM. There, except for the low-hanging fruit, a subagent's useful output is not a list of bugs. You can have a provably secure module A and a provably secure module B whose composition is still not secure. So, hunting for bugs in that "isolated" mode cannot catch meaningful bugs. Instead, a subagent's output should be knowledge about a module: what it assumes, what it delegates to its callers, and what invariant it is silently relying on. However, representing that kind of output well is the hard part. Too short and it skips the implementation detail that the bug actually sits on. Too long and it overflows the main agent's context before it can be combined with anything else. From what we have seen, at least at the time of writing, this problem is not solved efficiently by the agentic coding tools mentioned above. With that hypothesis in mind, we decided to run zkao on OpenVM, even though our original rule for these experiments was to run zkao only after the plain LLMs had already found a real bug. We have spent a lot of time on context engineering for zkao, and we have encoded the working methods of our own experts into it as reusable flows for finding vulnerabilities, so it seemed like the right tool for exactly this situation. After more than nine and a half hours of scanning, it returned many findings. Similar to the prior experiment, we did not have time to go over every finding in depth. After a quick pass, one stood out immediately: a critical soundness bug in the pairing check in one of the guest libraries. Our hypothesis held up, and months of effort had paid off! Although there is only one bug to share, to stay consistent with the first post, here is the bug at a glance. Severities and fixes at a glance # Bug AI severity OpenVM severity Fix commit Found by 1 openvm-pairing pairing check missing proper subfield check on scaling factor Critical Critical a720e2c zkao This time, the AI severity and the maintainer severity agree. Bug 1: openvm-pairing pairing check missing proper subfield check on scaling factor Background Pairings are the engine under Groth16, PLONK with KZG, and BLS signatures. In all of these protocols, the verifier is usually not asking for one pairing value. It is asking whether a product of pairings is one: $$ \prod_i e(P_i, Q_i) = 1. $$ From that one yes-or-no answer, a verifier concludes that a SNARK proof is valid, that a KZG opening is correct, or that a signature verifies. So if a prover can make a false pairing product appear to be one, everything built on top of it is no longer sound. A pairing is a bilinear map $$ e : G_1 \times G_2 \to G_T, $$ where $G_1, G_2, G_T$ are abelian groups. In our case, $G_1$ and $G_2$ are elliptic-curve groups and $G_T$ is a multiplicative subgroup of $\mathbb{F}_{p^{12}}^{*}$. The most important property of pairing is bilinearity: $$ e([a]P, [b]Q) = e(P, Q)^{ab}. $$ This is why pairings are useful, but we will not actually need this property to understand the bug. So you can just ignore it. Computing a pairing $e(P, Q)$ has two main steps (except for Weil pairing). The first step is the Miller loop. It evaluates a Miller function $f_{r, Q}(P)$, which we can just view as a black box for simplicity. This stage outputs an element in $\mathbb{F}_{p^{12}}^{*}$. For a product of pairings, the circuit can run all the Miller loops and multiply their outputs together. Let us call that combined output $f$, i.e. $f = \prod_i f_{r, Q_i}(P_i)$. The catch is that $f$ is not the pairing product yet. It is only one representative of the equivalence class $\mathbb{F}_{p^{12}}^{*} / (\mathbb{F}_{p^{12}}^{*})^r$. This is one of the main observations from Novakovic and Eagen's paper : Miller-loop outputs are unique only up to multiplication by an $r$-th power. In other words, $f_1$ and $f_2$ stand for the same pairing whenever there is a non-zero $c$ with $f_1 = f_2 \cdot c^r$. That undetermined factor $c$ is what makes a direct equality check tricky. That is why the second step exists. The final exponentiation raises $f$ to $$ h = \frac{p^{12} - 1}{r}. $$ This removes the ambiguity, because for every non-zero $c$, we have: $$ (f \cdot c^r)^h = f^h \cdot c^{p^{12}-1} = f^h. $$ The last term disappears because every non-zero element of $\mathbb{F}_{p^{12}}$ satisfies $x^{p^{12}-1} = 1$. After this exponentiation, the result lands in $G_T$, the group of $r$-th roots of unity. So the real pairing-product check is: $$ f^h = 1. $$ The problem is that the exponent $h$ is far too expensive to raise to inside the circuit, while it is fine to let the prover compute $c$ outside the circuit and pass it in as a hint. Checking $f = c^r$ is much cheaper than computing the $f^h$ exponentiation. So to prove $\prod_i e(P_i, Q_i) = f^h = 1$, instead of computing $f^h$ directly, the prover only needs to provide a non-zero $c$ with $f = c^r$, which holds exactly when $f^h = 1$. This is the core idea behind the optimization. OpenVM implements it with the residue-witness trick from Novakovic and Eagen's paper : the prover supplies a few extra values and the circuit checks a cheap equation instead of running the full exponentiation. The actual optimized equation is slightly different from $f = c^r$: $$ f \cdot u = c^{\lambda} \wedge u^{d^i} = 1$$ Here $\lambda = m \cdot r$ is a curve-specific exponent whose structure lets the circuit evaluate $c^\lambda$ cheaply through the Frobenius map, and $u$ is called the scaling factor. In the OpenVM code this scaling factor is called u for BN254 and s for BLS12-381. The remaining symbols are $d = \gcd(m, h)$ and $i = v_d(h)$. The scaling factor is needed because $\lambda$ does not line up perfectly with the original $r$-residue che