arrayref 공급망 공격과 proc-macro1 타이포스쿼트

지원 종료(EOL)된 PHP용 php2shell 패치(CVE-2026-109618 및 CVE-2026-109619)가 출시되었습니다. 수요가 많아 평소보다 영업팀의 답변이 늦어지고 있으므로, 패치된 패키지를 가장 빠르게 확보하려면 온라인에서 Runtimes용 ELS를 구매하시기 바랍니다 (새 탭에서 열림). 패치 FAQ 읽어보기

이 블로그 글의 AI 요약으로 바로 가기

  • Perplexity를 사용하여 이 게시물을 요약해 주세요
  • Gemini로 이 게시물을 요약해 주세요

요약

전체 버전으로 이동 ↓

사건 개요: 2026년 8월 20일, 한 공격자가 정식 유지보수자의 crates.io 계정을 이용해 arrayref 0.3.10, append-only-vec 0.1.9, internment 0.8.7의 악성 버전을 공개했습니다. 이 세 라이브러리의 소스 코드 자체는 문제가 없었습니다. 각 라이브러리는 David Tolnay의 proc-macro2를 표절(typosquat)한 proc-macro1에 대한 의존성을 추가했는데, 이 proc-macro1의 build.rs 파일은 base64로 인코딩된 공격자 주소를 재구성하고, 인증서 확인 없이 TLS를 통해 2단계 바이너리를 다운로드한 뒤, 컴파일 과정에서 이를 실행했습니다.

영향: arrayref는 crates.io에서 누적 다운로드 수가 2억 4,500만 건에 달하며, 이 중 약 5,370만 건은 지난 90일 동안 기록된 것이며, 403개의 크레이트가 이를 직접 의존성으로 명시하고 있습니다. 이 라이브러리는 암호화, 그래픽, 블록체인 라이브러리 내에 포함되어 있기 때문에 대부분의 팀은 이를 직접 선택하지 않았습니다. StepSecurity의 분석에 따르면, arrayref의 노출 기간은 UTC 기준 07:15에 공개된 시점부터 08:41에 인덱스에서 제거될 때까지 총 86분으로 추정됩니다. append-only-vec과 internment는 누적 다운로드 수가 각각 약 450만 건과 1,440만 건으로 더 적으며, 동일한 시간대에 한꺼번에 노출되었습니다.

현황: crates.io는 공격자가 생성한 6개의 크레이트(proc-macro1, proc-macro-en, aovine, arone, aronenao, tinymember)를 삭제하고, 악성 버전 3개를 제거했으며, 유지보수자의 계정을 잠갔습니다. Rust 보안 대응팀은 해당 작성자의 “컴퓨터나 인증 정보가 유출되었을 가능성이 높다”고 밝혔습니다. 레지스트리 정리는 사용자의 시스템을 청소해 주지 않습니다. Cargo 캐시, 벤더링된 트리, 컨테이너 레이어, CI 이미지 및 미러에는 여전히 해당 크레이트가 남아 있을 수 있습니다. 해당 날 아침에 호스트가 이러한 크레이트 중 하나에 도달했을 수 있는 새로운 의존성 그래프를 해결했다면, 확인이 완료될 때까지 이를 의심스러운 것으로 간주하십시오.

자세한 내용

저도 영향을 받나요?

여기서 중요한 질문은 두 가지이며, 두 질문의 답은 서로 다릅니다. crates.io가 그날 아침 악성 버전을 제거하면서 해당 버전을 획득할 수 있는 기회는 사라졌습니다. 하지만 이를 실행할 수 있는 기회는 여전히 남아 있었습니다. 제거나 삭제는 이미 디스크에 존재하는 내용을 지우지 않기 때문에, Cargo 캐시, 벤더링된 종속성 트리, 레지스트리 미러, 또는 이러한 버전 중 하나를 보유한 컨테이너 레이어 등은 여전히 이후 빌드에 이를 공급할 수 있습니다. 획득은 시간적으로 제한되어 있었지만, 실행은 사용자의 시스템이 가진 속성입니다.

실제 상황을 살펴보면, 노출된 그룹은 다운로드 수치가 시사하는 것보다 더 좁습니다. 확정된 Cargo.lock 파일은 정확한 버전을 고정하며, Cargo는 가능한 경우 종속성을 락파일의 버전으로 고정해 두기 때문에, 변경되지 않은 락파일로 빌드된 프로젝트는 arrayref 0.3.9를 계속 사용했고 0.3.10은 전혀 접하지 않았습니다. 이는 클린 컨테이너에서도 마찬가지입니다. 빌드 과정에서 유효한 락파일이 복사된다면(이는 일반적인 패턴입니다), 새로운 파일 시스템은 아무런 영향을 미치지 않습니다.

실제로 팀들을 위험에 빠뜨린 것은, 악성 버전이 활성화된 상태에서 영향을 받은 크레이트 중 하나에 도달할 수 있는 의존성 그래프에서 재해결이 이루어졌기 때문이었습니다. 이러한 상황에서 `cargo update`를 실행한 사람은 물론, 그날 아침 의존성 관리 봇이 업그레이드 풀 리퀘스트를 열어 병합한 사람도 모두 위험에 노출되었습니다. 또한 락파일(lockfile)이 없는 상태에서 빌드를 실행한 사람도 마찬가지였는데, 이는 실제 컨테이너 위험 요소입니다. 매니페스트만 복사하는 Dockerfile이나 Cargo.lock을 제외하는 .dockerignore 파일은 매번 처음부터 다시 해결을 수행하기 때문입니다. 그날 아침 매니페스트가 변경되어 arrayref에 대한 새로운 경로가 추가된 사용자도 마찬가지입니다. Cargo는 그래프의 영향을 받은 부분을 다시 해결하고 새로운 락파일을 생성하기 때문입니다. 또한 `–locked` 옵션을 지정하지 않는 한 게시된 락파일을 무시하는`cargo install`을 실행한 사용자도 마찬가지입니다.

라이브러리 작성자들은 애플리케이션 작성자들보다 더 많은 노출에 직면해 있었지만, 이를 의무화하는 규칙은 없었습니다. Cargo 팀은 2023년 8월에 기존의 “바이너리는 락파일을 커밋하지만, 라이브러리는 그렇지 않다”는 지침을 폐지했으며, 그 이후로 `cargo new`는 라이브러리에 대해서도 Cargo.lock을 추적해 왔습니다. 하지만 많은 라이브러리 저장소는 이 변경 이전의 것이며, 여전히 CI에서 새로 락파일을 생성하고 있습니다. 또한 락파일은 하위 단계로 전달되지 않습니다. 여러분의 락파일은 크레이트 사용자에게 영향을 미치지 않으며, 오직 Cargo.toml만이 영향을 미칩니다.

먼저 로컬 Cargo 캐시부터 확인해 보세요. Rust 보안 대응팀(Rust Security Response Team)이 다음과 같은 검사 방법을 공개했습니다:

find ~/.cargo/registry/cache -type f -name'append-only-vec-0.1.9.crate'-o-name'arrayref-0.3.10.crate'-o-name'internment-0.8.7.crate'-o-name'proc-macro1-*.crate'-o-name'proc-macro-en-*.crate'-o-name'aovine-*.crate'-o-name'arone-*.crate'-o-name'aronenao-*.crate'-o-name'tinymember-*.crate' -print

그런 다음 잠금 파일에서 동일한 버전이 있는지, 그리고 proc-macro1 항목이 있는지 확인해 보십시오. Cargo.lock 파일에 proc-macro1 행이 있는 것만으로도 이미 중요한 단서입니다. proc-macro1은 공격자가 이번 공격을 위해 생성한 것이며, 레지스트리에서는 이미 삭제되었기 때문입니다.

배열 참조(arrayref) 및 proc-macro1 공격이란 무엇인가

이 작업은 페이로드 배포 몇 시간 전부터 시작되었습니다. StepSecurity의 타임라인에 따르면, 실제 proc-macro2를 관리하는 데이비드 톨네이(David Tolnay)를 사칭한 ‘dtolney’라는 이름의 GitHub 계정이 UTC 기준 01:17에 생성되었습니다. 이어 UTC 기준 01:25에는 이와 일치하는 crates.io 계정이 생성되었습니다. UTC 기준 01:55에 해당 계정은 위조된 작성자 메타데이터가 포함된, proc-macro2의 깨끗한 복사본인 proc-macro1 1.0.106을 공개했습니다. 이 릴리스는 악의적인 행위를 하지 않았습니다. 5시간 이상 그 자리에 머물며 약간의 레지스트리 이력을 쌓았고, 별다른 특징이 없는 크레이트처럼 보였습니다.

UTC 기준 07:11에 동일한 계정이 proc-macro1 1.0.107을 게시했습니다. 이것이 바로 악용된 버전입니다. 4분 뒤인 UTC 기준 07:15에, 정당한 소유자의 계정에서 arrayref 0.3.10이 등장했는데, 이 버전에는 타이포스쿼트(typosquat)에 대한 종속성이 포함되어 있었습니다. 이어 UTC 기준 07:34에 internment 0.8.7이, 07:37에는 append-only-vec 0.1.9가 게시되었습니다.

같은 날 공개된 Rust 보안 대응팀(Rust Security Response Team)의 보고서에 따르면, UTC 기준 07:15에 proc-macro1이 악성이라는 제보를 받았으며, 널리 사용되는 arrayref 크레이트가 최근 재게시되면서 이를 참조하고 있었다고 밝혔습니다. 이 문제를 최초로 발견한 공로는 Nextron Systems GmbH의 연구팀에게 돌아갑니다. 이와는 별도로, jhobern이라는 닉네임을 사용하는 한 제보자가 RustSec 자문 데이터베이스에 이슈를 등록했으며, Rust 보안 팀에도 이메일을 보냈다고 밝혔습니다. StepSecurity의 재구성 결과에 따르면, 해당 RustSec 보고서는 악성 코드가 게시된 지 39분 후인 UTC 기준 07:54에 접수된 것으로 나타납니다. 이는 두 보고서가 동일한 경로가 아닌 서로 다른 신고 경로를 설명하고 있음을 시사합니다.

정리 작업은 UTC 기준 08:03에 proc-macro1을 삭제하는 것으로 시작되었습니다. 이후 Rust 보안 대응팀은 세 개의 정상 크레이트에 대한 정확한 제거 시간을 공개했습니다. arrayref 0.3.10은 온라인 상태 86분 만인 UTC 08:41에, internment 0.8.7은 90분 만인 UTC 09:04에, append-only-vec 0.1.9는 107분 만인 UTC 09:25에 제거되었습니다. crates.io 기록에 따르면, 공격자가 삭제했던 정상 버전이 세 크레이트 모두에 대해 UTC 기준 09:15부터 09:26 사이에 복원된 것으로 나타났습니다.

‘양키’가 어떻게 ‘루어’가 되었는가

이전의 npm 사고들 중에는 이번에 일어난 일과 유사한 사례는 없었습니다. 0.3.10 버전이 공개된 지 1분도 채 되지 않아, 공격자는 소유자의 계정을 이용해 약 4초 간격으로 스크립트를 실행해 arrayref의 최근 버전들을 모두 ‘yank’했습니다. Cargo에서 ‘yank’를 수행해도 버전이 삭제되지는 않습니다. 이미 ‘yank’된 버전을 참조하고 있는 기존 락파일(lockfile)은 계속 빌드되며, 리졸버는 새로운 버전을 선택할 때만 ‘yank’된 릴리스를 건너뜁니다. crates.io 버전 기록에 따르면, 07:15:24부터 07:15:40(UTC) 사이에 소유자 계정을 통해 0.3.5부터 0.3.9까지가 철회되었으며, 이후 09:15부터 09:17(UTC) 사이에 crates.io 팀원이 이를 복원했습니다. UTC 기준 07:38에 append-only-vec 0.1.7 및 0.1.8, 그리고 07:37에 internment 0.8.3부터 0.8.6까지에서도 동일한 일이 발생했습니다.

이로 인해 일상적인 레지스트리 유지보수 작업이 사회공학적 수단으로 변모하게 되었으며, 그 영향 범위가 좁았기 때문에 오히려 효과가 있었습니다. ‘yanked’ 처리 시 누구에게도 알림이 전송되지 않으며, 이미 ‘yanked’ 버전에 고정된 프로젝트는 해당 버전을 사용해 빌드를 계속 진행합니다. 'yanked-version' 경고는 Cargo가 레지스트리 인덱스와 통신할 때, 즉 `cargo update`, `cargo add` 또는 `cargo install --locked`와 같은 명령어를 실행할 때 표시되므로, 애초에 재해결을 수행해야 할 가능성이 가장 높은 사람들에게 가장 먼저 전달됩니다. 이들에게 악의적인 릴리스는 Cargo가 허용하는 최신 버전이 되었으며, ^0.3.5와 같은 일반적인 호환성 요구 사항의 경우, 이는 'yanked'되지 않은 유일한 후보였습니다. 경고를 확인하고 이를 제거하기 위해 `cargo update`를 실행한 개발자는 자신이 책임감 있는 행동을 하고 있다고 합리적으로 생각할 수 있습니다.

빌드 스크립트 내부

악성 코드는 arrayref가 아닌 proc-macro1의 build.rs 파일에 포함되어 있었습니다. arrayref 0.3.10 소스 코드를 확인해 본 사람이라면 누구나, 이 크레이트가 예전부터 항상 포함해 온 것과 동일한 배열 참조 매크로와 더불어 Cargo.toml에 추가된 하나의 의존성 항목만 발견했을 것입니다.

이 빌드 스크립트는 읽기 가능한 URL을 하드코딩하는 대신 base64 조각들을 조합하여 명령 및 제어(C&C) 정보를 구성한 뒤, 인증서 검증을 명시적으로 비활성화한 상태에서 TLS를 통해 https://23.254.165.112:9089/에서 2단계 바이너리를 가져왔습니다. 다운로더는 검증 방법이 무조건 성공을 반환하는 ‘all-accept’ 검증기를 사용하는 rustls를 활용했기 때문에, 연결은 암호화되었으나 실질적인 서버 인증은 제공되지 않았습니다. 동일한 호스트의 두 번째 주소인 23.254.165.112:443이 페이로드에 명령 및 제어 엔드포인트로 전달되었습니다. StepSecurity가 보고한 관련 호스트명은 hwsrv-798836.hostwindsdns.com입니다.

실행 방식은 플랫폼에 따라 달라졌습니다. 유닉스 시스템에서는 페이로드가 /tmp/rust-setup에 기록되고 실행 권한이 부여된 후, 분리된 프로세스로 실행되었습니다. 윈도우에서는 %TEMP%\rust-setup.ps1 파일을 생성한 뒤, wscript.exe를 통해 %TEMP%\rust-setup-launch.vbs 파일을 CREATE_NO_WINDOW 플래그와 함께 실행하여 화면에 아무것도 표시되지 않도록 했습니다.

이 문제가 알려지지 않은 데에는 두 가지 이유가 있습니다. 첫째, StepSecurity가 지적했듯이, proc-macro1에 있는 라이브러리 코드는 실제로 proc-macro2이기 때문에 빌드가 성공적으로 완료되고 일반 출력에는 이상한 점이 전혀 나타나지 않습니다. 둘째, 프로그램 내에서 배열 참조(arrayref) 함수를 호출할 필요가 전혀 없었고, 굳이 `cargo build`를 명시적으로 실행할 필요도 없었습니다. Cargo에는 설치 시 훅이 없기 때문에 다운로드 시에는 아무것도 실행되지 않았지만, 그래프를 컴파일하는 명령어를 실행하면 패키지가 컴파일되기 직전에 빌드 스크립트가 실행됩니다. `cargo check`, `cargo test`, `cargo clippy`, `cargo doc` 중 어느 하나만으로도 충분했으며, 이는 해당 명령어를 시작한 사용자나 시스템의 권한 수준에 따라 실행되었습니다. 개발자의 노트북에서는 SSH 키, 클라우드 인증 정보, 브라우저 데이터에 접근할 수 있는 사용자 세션이 됩니다. CI 환경에서는 레지스트리 토큰, 서명 자료, 클라우드 역할을 보유한 러너가 됩니다.

처음 몇 시간 동안은 아무도 2단계 바이너리가 실제로 어떤 기능을 하는지 알 수 없었습니다. RustSec, StepSecurity, SafeDep의 분석 보고서는 다운로더와 명령 및 제어(C&C) 주소만 기록하고 그 이상은 다루지 않았습니다. 하지만 이제 그 공백이 메워졌습니다. Wiz Research는 회수된 페이로드를 분석하여 기능이 뛰어난 백도어를 설명했습니다. 이 백도어는 HTTPS를 통해 비콘 신호를 보내고, 호스트 정보와 탈취한 인증 정보를 base64로 인코딩된 JSON 형식으로 전송하며, Chrome, Brave, Edge의 SQLite 데이터베이스에서 저장된 로그인 정보를 읽어오고, 리눅스에서는 systemd 사용자 서비스로, macOS에서는 LaunchAgent로, 윈도우에서는 레지스트리 Run 키로 지속성을 확보합니다. 이 백도어는 총 4개의 명령을 수행하며, 그중 하나는 임의의 셸 또는 PowerShell을 가져와 실행합니다. 또한 주 C2 서버가 응답하지 않을 경우 도메인 생성 알고리즘으로 대체됩니다. 따라서 실행이 확인된 경우 해당 호스트가 침해된 것으로 간주하고, 해당 호스트나 실행자가 접근할 수 있는 모든 인증 정보(브라우저에 저장된 비밀번호 포함)가 유출되었을 가능성이 있다고 가정해야 합니다.

인프라 관련 소식도 더욱 흥미로워졌습니다. 지난 6월 발생한 유사한 ‘마스트라(Mastra ) ’ 캠페인은 인증 정보, 브라우저 데이터, 암호화폐 지갑을 노리는 크로스 플랫폼 정보 탈취형 악성코드를 유포했는데, Wiz의 보고에 따르면 arrayref 페이로드는 마스트라가 사용했던 것과 동일한 C2 요청 경로로 비콘 신호를 보내며, 이 비콘 주소는 마스트라 호스트와 TLS 인증서 발급자를 공유하고, 이번 사건에서 피해자가 신고한 C2는 북한 행위자의 ‘액시오스(axios)’ 캠페인을 다루는 구글 위협 인텔리전스 자료에 등장한다고 한다. 마이크로소프트는 마스트라의 활동을 북한(DPRK) 클러스터의 소행으로 지목했습니다. 이는 이번 사건에 대한 공식적인 귀속 판단이라기보다는 문서화된 인프라 중복 사례에 가깝지만, 단순히 두 캠페인이 동일한 호스팅 제공업체를 공유하는 것보다는 훨씬 더 많은 공통점을 보여줍니다.

러스트의 빌드 모델이 왜 이 문제를 더 악화시키는가

Rust는 안전성 면에서 확고한 명성을 얻고 있으며, 이는 이 언어가 실제로 다루는 문제 영역에서 입증된 것입니다. 메모리 안전성과 엄격한 타입 시스템은 프로그램이 종료되기 전에 의존성이 의도적으로 임의의 코드를 실행하는 것을 막아주지는 못합니다.

Cargo는 빌드 시점에 사용자의 컴퓨터에서 코드를 실행할 수 있는 두 가지 방법을 의존성에 제공합니다. 첫 번째는 build.rs입니다. 이 파일을 패키지 루트에 두면 Cargo가 스크립트를 컴파일하여 “패키지를 빌드하기 직전”에 실행합니다. 이 기능의 정당한 용도는 C 라이브러리 컴파일, 코드 생성, 기능 탐지 등 실제적이고 흔한 작업들을 다루며, 바로 이 때문에 diff에서 별다른 주목을 받지 못합니다. 두 번째 방법은 절차적 매크로(proc macros)로, 코드가 컴파일되는 동안 컴파일러 내부에서 컴파일되고 실행됩니다. Rust 참조서(Rust Reference)는 그 결과에 대해 명확히 밝히고 있습니다. 절차적 매크로는 “Cargo의 빌드 스크립트와 동일한 보안 문제를 안고 있다”는 것입니다. 두 방법 모두 샌드박스 환경이 적용되지 않습니다.

npm 생태계에서는 적어도 `npm install –ignore-scripts`를 사용할 수 있는데, 이는 Mastra 공격 이후 이곳에서 제시된 완화 조치입니다. Cargo에는 안정판이나 나이트리 빌드에서 이에 상응하는 일반적인 기능이 없습니다. 임의의 종속성 빌드 스크립트를 단순히 비활성화하고 나머지 빌드 과정을 진행하도록 하는 빌드 플래그, 구성 키 또는 -Z 플래그가 없으며, 이러한 스크립트를 샌드박스에 격리하는 문제는 2018년부터 Cargo의 미해결 이슈로 남아 있습니다. 문서화된 유일한 제외 방법은 적용 범위가 매우 제한적입니다. links 키를 선언한 패키지의 경우, 구성에서 빌드 스크립트를 재정의할 수 있으며, 이 경우 해당 스크립트는 컴파일되지도 실행되지도 않습니다. 이는 C 라이브러리 메타데이터를 제공하는 -sys 크레이트에만 적용되며, proc-macro1과 같은 크레이트에는 해당되지 않습니다. 일반적인 플래그가 존재하지 않는 더 근본적인 이유는, 빌드 스크립트가 npm의 postinstall 훅처럼 설치 시 선택적으로 사용할 수 있는 편의 기능이 아니기 때문입니다. 이는 컴파일의 일부이며, 이를 비활성화하면 대개 패키지가 빌드되지 않게 됩니다.

전이적 거리도 중요합니다. arrayref는 일반적으로 배열 참조를 얻기 위한 매크로를 제공하는 리프 유틸리티 크레이트이며, 이 크레이트의 클린 릴리스는 런타임이나 빌드 종속성을 선언하지 않고 quickcheck에 대한 개발 종속성만 선언합니다. 바로 이 점이 0.3.10 버전을 특이한 사례로 만들었는데, proc-macro1이 이 크레이트가 역사상 처음으로 선언한 일반 종속성이었기 때문입니다. arrayref의 직접적인 종속 라이브러리로는 tiny-skia-path, tiny-skia, libsecp256k1, multiaddr, schnorrkel 등이 있으며, StepSecurity의 역방향 종속성 확인 결과 이더리움 측에서는 revm-precompile이, 솔라나 측에서는 solana-runtime과 spl-token이 추가되었습니다. 노출된 종속 라이브러리 중 가장 큰 규모를 차지한 것은 blake3로, 누적 다운로드 수가 약 1억 6,600만 건에 달했으며, 이 라이브러리는 1.8.6 버전까지 arrayref ^0.3.5를 요구했으나, 같은 날 오전 09:09(UTC)에 arrayref 없이 1.8.7 버전을 출시했습니다. 이러한 크레이트들 위에는 이를 사용하는 훨씬 더 많은 프로젝트들이 있습니다. 이 공격에 노출된 프로젝트 중 거의 어느 곳도 자체 Cargo.toml에 arrayref를 포함하고 있지 않았습니다. 이들은 그래픽 라이브러리나 암호화 라이브러리를 사용하고 있었는데, arrayref는 그 아래 두세 단계 아래, 즉 그들이 굳이 신경 쓸 이유가 없는 크레이트 내에 포함되어 있었습니다.

‘인테이크’에 대한 여러분의 생각을 바꿔줄 부분

이 문제를 포착했을 가능성이 있는 모든 검사 메커니즘은 완성된 결과물이 아닌 다른 대상에 대해 작동합니다.

버전 고정 기능은 도움이 되었고, 실질적인 보호 수단이지만, 이는 관성에 의한 보호에 불과합니다. 보안 업데이트가 필요한 순간까지는 제대로 작동하지만, 그 시점이 되면 문제를 해결하고 레지스트리가 그 순간 제공하는 내용을 그대로 적용하게 됩니다. 서명과 2단계 인증은 가치 있는 배포 제어 수단이지만, 둘 다 배포되는 코드가 무해하다는 것을 보장하지는 않습니다. Rust 보안 대응 팀은 유지보수자의 컴퓨터나 인증 정보가 유출되었을 가능성이 높다고 밝혔으나, 구체적으로 어느 쪽인지 공개적으로 확인하지는 않았기 때문에, 어떤 배포 제어 수단이 이번 사고를 막을 수 있었을지는 아직 단정할 수 없습니다. CVE 기반 구성 분석 스캔에서는 일치하는 항목이 없었으며, 이는 노출 기간 동안 해당 CVE가 존재하지 않았기 때문입니다. SBOM이 도움이 될 수는 있었겠지만, 단지 구성 요소 목록으로서만 가능했을 것입니다. proc-macro1은 빌드 전용 의존성이 아닌 일반 의존성으로 arrayref에 포함되었기 때문에, 컴파일 전에 생성된 Cargo 기반 또는 lockfile에서 파생된 SBOM은 proc-macro1 1.0.107을 새로 해결된 구성 요소로 나열했을 것입니다. 하지만 그 SBOM이 해당 구성 요소가 악성이라는 사실을 알려주지는 못했을 것입니다. 한 단계 더 아래로 내려가 보면, 실제로 다운로드를 수행한 크레이트인 base64, rustls, ureq는 proc-macro1의 빌드 종속성이었으며, 아티팩트 기반이거나 런타임에 중점을 둔 SBOM은 이러한 크레이트들을 완전히 생략했을 수도 있습니다.

소스 코드 검토는 Mastra 공격에서 실패했던 것과 동일한 이유로, 대부분의 팀이 실제로 작업하는 수준에서는 실패했을 것입니다. arrayref 0.3.10의 라이브러리 코드를 검토하는 사람은 깔끔하고 익숙한 매크로들을 발견하게 됩니다. 악의적인 행동은 한 단계 아래, 팀 내 그 누구도 선택하지 않은 종속성에 속한 빌드 스크립트 안에 있으며, 그 크레이트의 라이브러리 코드는 합법적인 코드의 복사본입니다. 매니페스트를 포함한 전체 릴리스 diff를 검토하는 검토자는 proc-macro2라는 이름에서 한 자리 숫자만 다른 새로운 종속성을 발견했을 것이며, 이를 통해 검토를 시작할 수 있는 단서를 얻었을 것입니다. 바로 이것이 제도화할 가치가 있는 검토 깊이이며, 대부분의 도입 프로세스가 운영되는 깊이와는 다릅니다.

방어 가능한 입장과 방어 불가능한 입장을 구분하는 기준은 신뢰 결정이 이루어지는 시점입니다. 컴파일이 이미 시작된 시점까지 미루면, 패키지가 빌드 호스트에 이미 존재하고 빌드 스크립트가 이미 실행되었을 가능성이 높기 때문에 때는 이미 늦을 가능성이 큽니다.

완화

처음 세 단계에 따라 나머지 과정이 단순한 정리 작업이 될지, 아니면 사고 대응이 될지가 결정되므로 이 순서대로 진행하십시오.

아티팩트를 찾으세요. 개발자 컴퓨터, 빌드 에이전트, 그리고 2026년 8월 20일에 빌드된 모든 컨테이너 이미지에서 위에서 설명한 Cargo 캐시 확인을 실행하세요. 그런 다음 모든 리포지토리의 락파일(lockfile)에서 arrayref 0.3.10, append-only-vec 0.1.9, internment 0.8.7 및 proc-macro1, proc-macro-en, aovine, arone, aronenao, tinymember가 포함된 모든 항목을 검색하십시오.

알려진 해시 값과 대조하여 확인하십시오. StepSecurity는 해당 아카이브에 대한 SHA-256 해시 값을 공개했습니다. arrayref 0.3.10의 해시 값은 25ad700976873c76af785cb99b33c48db7df8b81f21d1e9e06b3676b9a9373ae이며, proc-macro1 1.0.107의 해시 값은 61198155da51b838772eecf5bfaac6cbc4dcc388dccc56658fc28a8e831b34d4이며, 그리고 정상적인 미끼 버전인 proc-macro1 1.0.106은 b5c1b5b0763a8809a644a8f92224653f0aca623a98eecc714d27f74b80fbe436입니다. 검색 범위를 이 세 가지 모두로 좁히십시오. 버전 1.0.106에는 페이로드가 포함되어 있지 않으므로, 이를 발견했다고 해서 실행이 이루어졌거나 공격자의 인프라와 접촉이 있었다는 것을 의미하지는 않습니다. 그러나 이는 환경 내의 무언가가 스테이징 단계에서 타이포스쿼트를 해결했다는 유용한 징후입니다.

단순히 존재 여부뿐만 아니라 실행 여부를 추적하십시오. 엔드포인트에서 유닉스 호스트의 경우 /tmp/rust-setup을, 윈도우의 경우 %TEMP% 폴더 내의 rust-setup.ps1 또는 rust-setup-launch.vbs를 확인하십시오. 이는 드로퍼가 작성하는 로컬 파일 이름입니다. 프록시, 네트워크 또는 다운로드 텔레메트리에서는 rust-crate_0.1.0부터 rust-crate_0.4.0까지의 이름을 가진 요청을 별도로 검색하십시오. 이는 디스크에 해당 이름으로 나타나는 파일 자체가 아니라, 다운로더가 원격 측에서 선택하는 대상별 2단계 파일 이름입니다. 빌드 프로세스에서 wscript.exe가 생성되는지, 그리고 빌드 단계가 끝난 후에도 계속 실행되는 자식 프로세스가 있는지 확인하십시오. 캐시된 .crate 파일이 있다면 아카이브가 다운로드된 것입니다. 디스크에 드로퍼 아티팩트가 존재한다는 것은 빌드 스크립트가 페이로드를 작성할 만큼 충분히 실행되었음을 의미하며, 프로세스 또는 네트워크 텔레메트리를 통해 페이로드 자체가 실행되었는지 확인할 수 있습니다. 실행이 의심되는 경우, 백도어가 설치한 지속성 요소도 함께 찾아보세요. 즉, Linux에서는 인식되지 않는 systemd 사용자 서비스, macOS에서는 비정상적인 LaunchAgent, Windows에서는 예상치 못한 HKCU Run 항목 등을 확인해야 합니다.

아웃바운드 트래픽을 차단하고 검토하십시오. 23.254.165.112를 차단 목록에 추가하고, 스테이지 2 명령 및 제어 주소로 보고된 23.254.167.107 및 23.254.167.216도 함께 추가하십시오. 또한 규칙의 적용 범위를 관찰된 포트 9089 및 443으로만 제한하지 마십시오. hwsrv-798836.hostwindsdns.com도 차단 목록에 추가한 후, 프록시 및 방화벽 로그를 역추적하여 빌드 호스트에서 발생한 연결을 확인하십시오. 여기에는 2단계 비콘 엔드포인트인 /49890878 경로로 전송된 HTTPS 요청도 포함됩니다. 비표준 포트의 일반 IP 주소에 접속하는 빌드 프로세스는 이번 캠페인과 무관하게 조사할 가치가 있는 중요한 징후입니다.

빌드가 접근할 수 있는 항목을 대상으로 회전 처리를 수행하십시오. 페이로드가 실행된 경우, 크레이트의 동작이 아닌 해당 신원이 보유한 항목에 한해 회전 처리를 적용하십시오. 개발자 컴퓨터의 경우, 이는 SSH 키, 클라우드 인증 정보, 레지스트리 토큰, 쉘 환경을 통해 노출된 모든 비밀 정보, 그리고 Chrome, Brave 또는 Edge에 저장된 로그인 정보를 의미합니다. 페이로드가 이러한 정보를 직접 읽어들이기 때문입니다. CI 환경에서는 러너 비밀 정보, OIDC 신뢰 관계, 패키지 게시 토큰 및 서명 자료를 의미합니다.

전이적 종속성을 올바르게 고정하세요. arrayref 0.3.9는 클린 상태이며, unyank 처리되어 다시 사용할 수 있습니다. 자신의 매니페스트에 arrayref를 포함하는 경우는 거의 없으므로, arrayref = “=0.3.9”는 평소에는 사용하지 않는 직접적인 종속성을 추가하는 것을 의미하며, 정확한 고정(pin)은 나중에 종속성 그래프를 만족 불가능하게 만들 수 있습니다. 전이적 의존성의 경우, 더 깔끔한 방법은 `cargo update -p arrayref –precise 0.3.9`를 실행한 후, 생성된 락 파일을 커밋하고 적용하는 것입니다.

lockfile 관리 규정을 반드시 준수하도록 해야 합니다. –locked 옵션을 사용하여 CI를 실행하면, Cargo가 lockfile을 변경하는 대신 실패하도록 지시하므로, 예기치 않은 의존성 해결로 인해 빌드가 중단되어 배포가 이루어지지 않게 됩니다. 전이적 버전을 변경하는 dependency-bot 풀 리퀘스트에 대해서는 반드시 사람이 검토하도록 하고, 업그레이드 요청(yank 경고, 봇 풀 리퀘스트, 빨간색 CI 작업 등)이 발생하면 의존성을 다시 해결하기보다는 diff를 꼼꼼히 검토해야 합니다.

탐지

빌드 과정이 어떻게 진행되는지 지켜보세요. UTC 기준 07:11 시점에서는 CVE 피드와 취약점 데이터베이스에 proc-macro1에 대한 정보가 전혀 없었으며, 평판 데이터는 5시간 분량의 레지스트리 기록만을 바탕으로 분석해야 했습니다. 이때 행동 양상이 유일한 단서였습니다. 빌드 스크립트에서 예상치 못한 외부 네트워크 활동이 감지된다면 이를 보안 관련 문제로 간주할 가치가 있으며, 실행 파일을 임시 디렉터리에 작성한 후 연결을 끊는 빌드 프로세스 자체만으로도 강력한 징후가 됩니다.

규칙을 작성할 때는 프로세스 계보를 정확히 파악해야 합니다. 빌드 스크립트는 Cargo가 생성하는 별도의 컴파일된 실행 파일(일반적으로 대상 디렉터리 아래의 build-script-build)로 실행되므로, 신호는 Cargo 자체가 아니라 네트워크에 도달하는 Cargo의 일시적인 자식 프로세스에서 발생합니다. 반면, 절차적 매크로 페이로드는 rustc 내부에서 실행됩니다. 두 경우 모두에 대해 경고를 설정하고, 빌드 중에 /tmp 또는 %TEMP%에 새로 나타나는 실행 파일에도 경고를 설정하십시오.

실행이 의심되는 호스트의 경우, 탐지 범위를 빌드 시간 이후로 확대해야 합니다. 2단계 백도어는 플랫폼에 따라 systemd 사용자 서비스, LaunchAgent 또는 레지스트리의 Run 키를 통해 지속되며, HTTPS를 통해 신호를 전송하므로, 빌드 시점 전후로 예상치 못하게 생성된 지속성은 그 자체로 경고를 발령할 만한 가치가 있습니다.

의존성 계층에서는 락파일(lockfile) 차이 분석 결과에서 새로 나타난 의존성을 주의 깊게 살펴보세요. 특히, 널리 사용되는 크레이트와 이름에서 한 글자나 한 자릿수만 다른 의존성을 주의해야 합니다. ‘proc-macro1’ 대 ‘proc-macro2’의 차이가 핵심이며, 의존성 차이 분석 결과를 확인하는 사람이라면 누구나 차이 분석 결과에서 이를 명확히 파악할 수 있습니다. 이번 캠페인에서 사용된 빌드 시간 다운로더 크레이트인 base64, rustls, ureq는 proc-macro1 내부에서 한 단계 더 아래에 선언되었으므로, 자신의 직접적인 빌드 의존성만 검사하는 규칙으로는 이를 놓쳤을 것입니다.

Rust용 SecureChain이 결과를 어떻게 바꾸는가

이 공격이 성공한 이유는 빌드 시 실행 단계가 거의 모든 Rust 파이프라인에서 신뢰받고 있으며, 별다른 검토를 거치지 않는 단계이기 때문입니다. 바로 이 특정 취약점을 해소하기 위해 ‘SecureChain for Rust’가 개발되었습니다.

SecureChain은 강화되고 격리된 환경에서 모든 패키지를 소스 코드부터 재컴파일하며, 악의적인 build.rs 스크립트 및 절차적 매크로를 포함해 빌드 시점의 공격 패턴에 대해 크레이트 소스 코드를 구체적으로 검토하여, 영향을 받는 크레이트가 툴체인으로 유입되기 전에 차단합니다. 이러한 차이점은 일반적인 npm 사고보다 이번 사례에서 더욱 중요합니다. Cargo에는 –ignore-scripts와 동등한 옵션이 없기 때문에, Rust 팀은 빌드 스크립트를 비활성화할 수 없습니다. 따라서 검토는 파이프라인 내에서 비활성화하는 방식이 아니라 업스트림 단계에서 이루어져야 합니다. base64 조각으로 URL을 재구성하고, 인증서 검증을 비활성화하며, IP 주소만 있는 곳에서 바이너리를 가져오는 build.rs는 검토를 통해 포착해야 할 빌드 시점의 패턴에 해당하며, 크레이트가 개발자의 노트북에 도달하기 전에 이를 검토하는 것이 먼저 실행한 뒤 조사하는 것보다 훨씬 안전합니다.

Rust용 SecureChain 알아보기 →

SecureChain은 파이프라인 끝에 단순히 덧붙여진 스캐너가 아니라, crates.io에서 직접 가져오는 대신 소스 코드부터 재컴파일하여 검증된 대안으로 기능합니다. 패키지는 알려진 악성코드, 의심스러운 코드 패턴 및 공급망 위험에 대해 선별 검사를 거치며, 검사에 통과하지 못한 항목은 사용자의 환경에 도달하기 전에 차단됩니다. 이 서비스가 지적하는 Rust 특유의 위험 요소들은 이번 사건에서 드러난 것들과 동일하며, 여기에는 유사 크레이트, 해킹당한 게시자, 조작된 릴리스, 숨겨진 동작, 그리고 유지보수가 중단된 크레이트의 장악 등이 포함됩니다. 크레이트의 소유권이 이전된 후, 이전에 신뢰받던 이름으로 악성 업데이트를 배포하는 방식이 바로 그 예입니다. 기본 의존성의 이름이 한 자리만 다른 크레이트는 인테이크 모델이 의심스러운 것으로 간주하도록 설계된 전형적인 유사 패턴이며, 이러한 위험은 어떤 빌드에서도 해당 크레이트를 사용하기 전부터 이미 존재하고 있었습니다.

이러한 변화는 개발자의 업무 방식에는 아무런 영향을 미치지 않습니다. SecureChain은 JFrog Artifactory, Sonatype Nexus, GitHub Packages와 호환되는 드롭인 리포지토리 대체 솔루션으로, 팀은 기존과 동일한 패키지, API, Cargo 명령어를 계속 사용할 수 있습니다. 또한 SLSA 레벨 3 출처 정보, SBOM 및 VEX 출력도 제공합니다. SBOM은 정의된 범위 내에서 구성 요소와 의존성 관계를 목록화합니다. 출처 정보는 아티팩트가 어떻게, 어디서 빌드되었는지를 기록하며, 그 출처와 빌드 과정에 대한 증거를 제공합니다.

이 문제에는 ‘핀닝(pinning)’만으로는 해결할 수 없는 더 깊은 문제가 있습니다. arrayref는 한 번 작성된 후 10년 동안 수백 개의 의존 라이브러리에 조용히 포함되는, 작고 안정적인 유틸리티 크레이트입니다. 그런 크레이트를 면밀히 주시하는 팀은 거의 없으며, 결국 매우 많은 팀이 이를 그대로 배포하게 됩니다. 바로 이 점이 arrayref와 같은 크레이트를 매력적인 표적으로 만들고, 팀들을 버려진 의존성과 재작성 사이에서 곤란한 처지에 빠지게 하는 원인입니다. SecureChain은 지원 대상 구성 요소가 유효한 지원 기간 내에 있는 동안 SLA에 따라 모니터링하고 패치를 적용합니다. 특정 버전이 지원 종료(EOL) 상태에 도달하더라도, TuxCare의 ‘Endless Lifecycle Support’를 통해 해당 버전을 계속 사용하는 한 보안 수정 사항을 지속적으로 제공받을 수 있습니다. 강제 업그레이드도, 재작성도 필요하지 않습니다.

이것이 단순한 한 번의 캠페인보다 더 중요한 이유

다른 생태계, 똑같은 실패. 지난 6월 진행된 ‘Mastra’ 캠페인은 140개가 넘는 깨끗한 npm 패키지에 오타를 이용한 도메인 스쿼팅(typosquatting) 의존성 라인을 단 하나씩 추가했습니다. '미니 샤이-훌루드(Mini Shai-Hulud)' 공격 물결은 유효한 SLSA 빌드 레벨 3 출처 정보를 유지한 채 172개의 npm 패키지와 2개의 PyPI 패키지에 영향을 미쳤으며, 그 후속 공격인 '메갈로돈(Megalodon)'은 패키지 공개 단계를 완전히 건너뛰고 수천 개의 저장소에 GitHub Actions 워크플로를 대량으로 주입하여 클라우드 인증 정보, SSH 키 및 토큰을 탈취했습니다. 이번 공격은 동일한 수법을 Cargo로 확장하여, 해킹당한 관리자 계정, 타이포스쿼팅된 종속성, 그리고 Cargo 고유의 ‘yank’ 세미오틱스를 이용해 업그레이드 압박을 조성했습니다. Wiz는 이후 이 캠페인과 Mastra 간의 인프라 중복을 문서화했으며, Mastra는 북한 클러스터의 소행으로 밝혀졌으므로, 운영자는 동일할 수 있으나 생태계는 변화하고 있습니다.

이러한 사건들을 통틀어 볼 때, 침해된 대상은 전이적 의존성, 출처 정보가 확인된 악성 패키지, 또는 CI/CD 워크플로우 자체 등 각기 달랐습니다. 공통된 문제점은 인증된 게시자, 유효한 출처 정보, 또는 빌드 통과 여부를 입력과 빌드 과정에서 발생하는 동작이 무해하다는 증거로 간주한 것이었습니다. ‘미니 샤이-훌루드(Mini Shai-Hulud)’는 악성 아티팩트가 암호학적으로 유효한 SLSA 출처 정보를 포함하고 있었기 때문에 출처 정보의 한계를 명확히 보여줍니다. 동작은 이미 사용자의 자격 증명을 보유한 머신에서가 아니라, 빌드가 시작되기 전인 입력 단계에서 확인되어야 합니다.

Rust용 SecureChain 알아보기 →

자주 묻는 질문

Cargo.lock 파일을 제대로 설정해 두면 안전할까요?

악성 버전을 획득하는 데 있어서는 대체로 그렇습니다. 변경되지 않은 lockfile은 프로젝트를 이미 해결된 버전 상태로 유지하는데, 대부분의 팀의 경우 arrayref 0.3.9를 의미하며, lockfile을 복사해 오는 클린 컨테이너도 마찬가지입니다. 이러한 보호 기능은 cargo update, 의존성 봇, 크레이트에 대한 새로운 경로를 도입하는 매니페스트 변경, lockfile이 없는 상태에서 실행되는 빌드, 또는 –locked 옵션 없이 실행되는 cargo install 등을 통해 새로 해결하는 순간 종료됩니다. 또한, 사용자들이 해결하는 버전은 오직 귀하의 Cargo.toml에 의해서만 결정되므로, 귀하의 락파일은 사용자에게 전달되지 않습니다. 그리고 이 모든 과정은 캐시나 벤더링된 트리에 이미 존재하는 악성 버전에는 전혀 영향을 미치지 않습니다.

빌드 스크립트가 실행되지 않도록 막을 수 있을까요?

현실적으로 불가능합니다. 빌드 스크립트를 건너뛰는 안정판이나 나이트리 빌드용 Cargo 플래그는 없으며, 문서화된 유일한 예외인 ‘links’ 키를 선언한 패키지의 빌드 스크립트를 재정의하는 방법도 일반적인 해결책이 될 수 없습니다. npm install 후크와 달리, Rust 빌드 스크립트는 컴파일 과정의 일부이며, 정상적으로 작동하는 아티팩트를 생성하기 위해 필수적인 경우가 많습니다. 그렇기 때문에 검사 기능은 빌드 과정 내에서 비활성화하는 대신, 빌드 단계보다 더 상류 단계에서 수행되어야 합니다.

유지보수 담당자에게 책임이 있었나요?

Rust 보안 대응팀은 해당 작성자가 악의적인 의도를 가지고 행동한 것은 아니라고 밝혔으며, 가장 유력한 원인은 시스템 침해나 인증 정보 유출일 것이라고 설명했다. 예방 조치로 해당 계정은 잠금 처리되었다. 이는 ‘Mastra’ 공격과 동일한 유형의 보안 사고로, 당시 공격의 진입점은 약 16개월 동안 활동이 없었고 접근 권한이 취소되지 않은 전직 기여자의 npm 계정이었던 바 있다.

왜 이전 버전을 강제로 제거하는 것이 중요했을까요?

‘yanking’은 버전을 삭제하지 않으며, 기존 락파일(lockfile)을 기반으로 빌드가 계속 진행됩니다. 이 작업으로 변경되는 것은 새로운 버전 해결 결과뿐입니다. 리졸버는 이미 락파일에 포함된 릴리스가 아닌 한, 제거된 릴리스는 건너뛰기 때문입니다. 정상적인 버전을 제거함으로써 악성 버전이 새로운 버전 해결 시 허용되는 최신 릴리스로 남게 되었습니다. 또한 Cargo는 인덱스와 통신할 때 제거된 버전에 대한 경고를 표시하므로, 이에 대응하여 `cargo update`를 실행해 버전을 다시 해결한 개발자들이 0.3.10 버전을 적용할 가능성이 가장 높았습니다.

SBOM이나 CVE 스캐너라면 이 문제를 포착했을까요?

그 자체만으로는 아닙니다. 악성 버전이 유포되던 당시에는 일치하는 CVE가 없었습니다. 빌드 전에 생성된 SBOM이라면, proc-macro1 내부의 의존성이 일반 의존성으로 선언되어 있었기 때문에 이 예기치 못한 의존성을 파악할 수 있었을지도 모릅니다. 하지만 SBOM은 악성코드 탐지기가 아니라 단순히 구성 요소를 나열한 목록일 뿐입니다. 또한 일부 SBOM은 빌드 전용 의존성을 생략하기도 하는데, 바로 proc-macro1 내부에 포함된 다운로더 크레이트가 이에 해당했습니다. 탐지를 위해서는 새로운 의존성이나 그 동작을 인식할 수 있는 정책 또는 분석 계층이 여전히 필요했습니다.

이 점에서 Rust는 npm보다 보안 취약점이 더 많을까요, 아니면 더 적을까요?

Rust의 취약점 노출 방식은 다릅니다. 패키지 수가 적고 공격 표면도 상대적으로 작으며, 애플리케이션 개발 시 lockfile을 엄격히 관리하는 관행이 도움이 됩니다. 하지만 빌드 스크립트와 proc 매크로는 npm install 후크보다 피하기 어렵기 때문에, npm에서 라이프사이클 스크립트 실행을 억제하는 간단한 방식에 해당하는 직접적인 Cargo 기능은 없습니다.

가장 중요한 장기적인 해결책은 무엇일까요?

신뢰 결정 과정을 초기 검토 단계로 옮기십시오. 패키지가 환경에 유입되기 전에 재빌드하고 검사하며, 버전 간 차이점을 검토하고, 새로운 종속성을 보안 관련 변경 사항으로 간주해야 합니다. 또한, 출처 및 빌드 프로세스에 대한 증거로 프로비넌스를 요구하되, 이를 결과적인 동작이 무해하다는 증거로 간주해서는 안 됩니다.

4,500명 이상의 Linux 및 오픈 소스 전문가와 함께하세요!

한 달에 두 번. 스팸이 없습니다.

목차
필요한 오픈 소스 보안에 대한 답변 얻기

4,500명 이상의 Linux 및 오픈 소스 전문가와 함께하세요!

한 달에 두 번. 스팸이 없습니다.