
머신러닝 모델이 당신의 컴퓨터에서 코드를 실행하는 방식 (그리고 이를 스캔하는 방법)
요약
머신러닝 모델 로드 시 발생하는 pickle 역직렬화 공격의 위험성과 이를 우회하는 최신 기법을 분석합니다. 기존 스캐너의 한계를 극복하고 악성 페이로드를 탐지하는 새로운 보안 도구인 Airlock의 구축 과정을 다룹니다.
핵심 포인트
- pickle 형식은 역직렬화 과정에서 임의의 Python 코드를 실행할 수 있는 취약점이 있음
- Hugging Face 모델의 약 45%가 여전히 보안에 취약한 pickle 형식을 사용 중
- 파일 확장자 변경 등을 통한 기존 스캐너(picklescan) 우회 기법 존재
- 새로운 보안 도구 Airlock은 높은 탐지율과 낮은 오탐률을 기록함
pickle 역직렬화 (deserialization) 공격, 2025년 스캐너 우회 기법, 그리고 이를 견뎌내는 스캐너 구축에 대한 바이트 단위 탐구.
저자: Mohit Kumar
프로젝트: Bulwark – AI 에이전트를 위한 오픈 소스 보안 스택
GitHub: mk12002/Bulwark
이 기사는 AI 시스템, 에이전트 워크플로우(agentic workflows), 그리고 AI 소프트웨어 공급망을 보호하기 위한 실질적인 접근 방식을 탐구하는 Bulwark 시리즈의 일부입니다.
torch.load("pytorch_model.bin")는 파일을 읽는 것처럼 보입니다. 하지만 그렇지 않습니다. 인터넷에 있는 수많은 모델의 경우, "가중치(weights)를 로드하는 것"과 "공격자의 Python 코드를 실행하는 것"은 _동일한 작업_입니다. 그리고 2025년, 연구자들은 단순히 파일 이름을 바꾸는 것만으로 업계 표준 스캐너를 그대로 통과할 수 있는 CVSS 점수 9.3의 우회 기법을 발견했습니다.
저는 이 문제의 심연을 파고들어 스캐너를 만들어냈습니다. 공격이 어떻게 작동하는지, 왜 표준 방어 체계가 무너졌는지, 그리고 그들이 놓친 회피 기법들을 잡아내기 위해 무엇이 필요한지에 대한 모든 바이트를 공개합니다.
요약 (TL;DR)
Python의
pickle형식은 아주 작은 스택 가상 머신(stack VM)입니다. 임의의 호출 가능한 객체(callables)를 호출하는 오퍼코드(REDUCE)를 가지고 있어, pickle은 역직렬화(deserialization)되는 즉시os.system("...")을 실행할 수 있습니다.인기 있는 Hugging Face 모델의 약 45%가 여전히 pickle을 사용하고 있습니다 (CCS 2025). 신뢰할 수 없는 소스에서 이를 로드하는 것은 신뢰할 수 없는 프로그램을 실행하는 것과 같습니다.
2025년, Hugging Face가 실행하는 스캐너인
picklescan이 CVE-2025-10155를 포함한 여러 우회 공격을 받았습니다. 예를 들어,evil.pkl의 이름을model.safetensors로 바꾸면 확장자 기반 검사를 통과해 버립니다.저는 콘텐츠를 통해 pickle을 _정적(statically)_으로 역어셈블하고, 회피 계층을 디코딩하며, 형식 스푸핑(format spoofing)을 탐지하는 Airlock을 구축했습니다. 14개의 페이로드를 가진 적대적 제품군(adversarial suite) 테스트에서 Airlock은 14/14점을 기록한 반면, picklescan은 10/14점을 기록했습니다. 또한 18개의 실제 선량한 모델에 대해 오탐(false alarms)은 0건이었습니다.
이것이 중요한 이유
코드베이스에 있는 모든 from_pretrained()는 신뢰에 대한 결정입니다. 모델 파일은 수동적인 데이터가 아닙니다. 가장 흔한 직렬화 (serialization) 형식의 경우, 그것은 하나의 프로그램이며, 이를 역직렬화 (deserializing)하는 것은 해당 프로그램을 실행하는 것을 의미합니다. 보안 커뮤니티는 "신뢰할 수 없는 데이터를 언피클 (unpickle) 하지 마라"는 말을 민간 전승(folklore)처럼 취급하지만, 전체 ML 생태계는 누구나 업로드할 수 있는 공개 허브로부터 매일 수백만 번씩 정확히 그 행위를 수행하는 것을 기반으로 구축되어 있습니다.
그 결과, 실시간으로 악용 가능한 공격 표면 (attack surface)이 형성됩니다. ReversingLabs와 다른 기관들은 실제 환경에서 발견된 악성 모델들을 찾아냈습니다. 이것은 사고 실험이 아닙니다. 이는 AI 공급망 (supply chain)에서 가장 직접적인 코드 실행 (code-execution) 경로입니다.
핵심 개념: pickle은 데이터 형식이 아니라 스택 VM입니다
대부분의 사람들은 pickle을 "Python 객체를 위한 JSON"으로 생각합니다. 하지만 실제로는 작은 스택 머신 (stack machine)을 위한 직렬화된 프로그램입니다. 언피클 (unpickle)을 할 때, 인터프리터는 일련의 옵코드 (opcodes) 스트림을 따라갑니다: 이 문자열을 푸시(push)하고, 저 전역 변수(global)를 조회하고, 저 인자들을 사용하여 이 호출 가능 객체(callable)를 호출하는 식입니다.
위험한 옵코드는 GLOBAL / STACK_GLOBAL (module.name 참조를 해결 — 예: os.system)과 REDUCE (스택 상단의 객체를 그 아래의 인자 튜플과 함께 호출)입니다. 이들을 결합하면 데이터로 인코딩된 임의 코드 실행 (arbitrary code execution)이 가능해집니다.
다음은 Python으로 작성된 전체 익스플로잇 (exploit)입니다. __reduce__ 메서드는 pickle에게 로드 시 정확히 무엇을 호출할지 알려줍니다:
import pickle, os
class Exploit:
...
누군가가 해당 파일에 대해 torch.load()를 실행하면, os.system이 실행됩니다. 경고도, 샌드박스 (sandbox)도 없습니다. 비유하자면, pickle 파일은 "조립 지침"이 불을 지르도록 허용된 트로이 목마와 같습니다. 지침을 읽는 것 자체가 불을 지르는 행위입니다.
실제 작동 방식 — 바이트 단위의 공격
최소한의 악성 pickle을 역어셈블 (disassemble)해 보겠습니다. Python 자체의 pickletools는 옵코드 (opcodes)를 보여줍니다 (이것은 파싱 (parse)을 수행하는 것이지, 실행하는 것이 아닙니다):
0: \ PROTO 4
2: \ FRAME ...
14: \ SHORT_BINUNICODE 'os' # 모듈 이름 푸시
...
그게 전부입니다. 네 개의 의미 있는 옵코드 (opcode)가 "가중치 파일 (weights file)"를 명령 실행 도구로 탈바꿈시킵니다. 스캐너의 역할은 이와 동일한 옵코드 스트림 (opcode stream)을 따라가며 os.system (위험한 호출 가능 객체)이 REDUCE에 도달하는 것을 포착하는 것입니다.
2025년 표준 스캐너가 무너진 지점
picklescan은 정확히 그 과정을 수행하며, 이를 매우 잘 해냅니다. 하지만 공격자들은 디스어셈블러 (disassembler)를 공격하지 않습니다. 그들은 디스어셈블러
_주변의 모든 것_을 공격합니다. 2025년에 일련의 우회 기법들이 등장했습니다:
[
CVE-2025-10155 사례는 거의 우스꽝스러울 정도입니다: picklescan은 파일 확장자를 기반으로 스캐너를 선택했습니다. malicious.pkl을 model.bin 또는 model.safetensors로 이름을 바꾸면 파일 유형을 잘못 분류하여 스캔을 시작하는 데 실패했습니다. 이후 세 개의 제로데이 (zero-day) 취약점(2025년 9월 picklescan 0.0.31에서 수정됨)과 2025년 12월 Sonatype에서 발견한 네 개의 취약점이 뒤를 이었습니다. Cisco는 피클 (pickle) 스캐너를 강화하기 위해 _구조 인식 퍼징 (structure-aware fuzzing)_을 발표하기까지 했습니다. 여기서 얻는 교훈은 오래되었지만 변치 않는 진리입니다: 콘텐츠 대신 메타데이터 (확장자)를 신뢰하는 것이 바로 스캐너가 실패하는 방식입니다.
Airlock 구축 — 회피를 상정하고 생존하도록 설계된 스캐너
저는 단 하나의 규칙을 중심으로 Airlock을 구축했습니다: 확장자를 절대 신뢰하지 마라; 콘텐츠를 디스어셈블 (disassemble)하고, 회피 계층 (evasion layers)을 먼저 벗겨내라.
[
각각의 우회 클래스를 겨냥한 세 가지 설계 선택 사항이 중요합니다:
1. 콘텐츠 스니핑 (Content sniffing)이 확장자 스푸핑 (extension spoofing)을 이깁니다 (CVE-2025-10155 방어)
Airlock는 매직 바이트 (magic bytes)를 읽습니다. 만약 파일의 _확장자 (extension)_는 안전한 형식(.safetensors, .gguf)이라고 주장하지만, 그 _바이트 (bytes)_가 피클 스트림 (pickle stream)이라면, 그것은 우연이 아니라 우회 (bypass) 시도입니다. Airlock은 이러한 기만 행위를 플래그 처리(M6)함과 동시에 숨겨진 피클을 어쨌든 역어셈블 (disassemble)하므로, 페이로드 (payload)는 여전히 M1을 트리거합니다. 확인 결과:
$ airlock scan model ./disguised # model.safetensors라는 이름을 가진 pickle
CRITICAL M1 Pickle references a shell/exec callable os.system @ model.safetensors
HIGH M6 File content does not match its extension model.safetensors
그리고 결정적으로, 진정한 safetensors 파일은 **제로 (zero)**의 오탐 (false positive)을 발생시킵니다. 확인 단계에서 스트림이 실제 임포트 (import)를 포함한 피클로 실제로 역어셈블되어야 하기 때문입니다.
2. 회피 계층 벗겨내기 (Peel the evasion layers)
.bin 파일 내부의 gzip 내부의 pickle이라고요? Airlock은 먼저 제한된 범위의 압축 해제 (bounded decompression)를 수행합니다. 외부 피클 내부의 base64 문자열로 숨겨진 페이로드(전형적인 "스테이지드 (staged)" 레이아웃)라면? 한 단계 깊이까지 base64 형태의 문자열을 디코딩하고 다시 스캔합니다. 단순한 c os system grep을 피하기 위해 os와 system을 별도의 옵코드 (opcode)로 분리한 STACK_GLOBAL은 어떻게 될까요? 메모리 인지형 스택 (memo-aware stack)이 여전히 이를 해결합니다.
3. 화이트리스트 (Allowlist) 모드 — 알려진 것뿐만 아니라 새로운 것도 포착 (Fickling 스타일의 아이디어)
차단 목록 (Denylists)은 이미 목격한 공격만을 잡아낼 수 있습니다. Trail of Bits의 Fickling은 2025년에 이를 뒤집었습니다: 알려진 안전한 임포트를 허용하고, 그 외의 모든 것을 플래그 처리하십시오. Airlock의 --strict 모드는 이를 모듈 수준에서 수행합니다. 저는 경험적으로 안전한 집합을 도출했습니다. 제가 테스트한 18개의 실제 모델 피클을 통해 확인한 결과, 이들은 정확히 두 개의 모듈에서 임포트되었습니다:
54개의 실제 pickle 스트림 전반에 걸친 최상위 모듈 (TOP-LEVEL MODULES):
135 torch
51 collections
따라서 --strict는 ML 허용 목록(torch, numpy, collections, …) _외부_의 모든 pickle 임포트를 플래그합니다. 이는 denylist가 통과시킬 수 있는 새로운 socket.gethostname이나 이전에 본 적 없는 악성 모듈을 포착해냅니다. 실제 코퍼스에서는 오탐(false positives)이 0개 발생했는데, 그 이유는 실제 가중치들이 실제로 torch/collections에서만 임포트되기 때문입니다.
실제로 작동할까? 벤치마크 결과.
주장은 싸구려입니다. 저는 프로토콜 0–5, 프레임드 pickle, STACK_GLOBAL 분할, gzip/zlib, base64 스테이징, numpy-object 스머글링, torch-zip, 그리고 위장된 .safetensors를 포함하는 14개의 페이로드로 구성된 **적대적 테스트 세트(adversarial suite)**를 구축했습니다. (모든 페이로드는 무해합니다 — 피해를 주는 대신 마커를 echo할 뿐이며, 절대로 unpickle되지 않습니다.) 그리고 이 모든 것과 18개의 실제 모델을 대상으로 Airlock과 picklescan을 실행했습니다.
| 그룹 | Airlock | picklescan |
|---|---|---|
| 적대적 페이로드 (14개 회피 페이로드) | 14/14 | 10/14 |
실제 모델 (18개 무해한 .bin) | 0/18 | 0/18 |
제가 솔직하게 말씀드리고 싶은 두 가지가 있습니다:
- Airlock의 강점은 gzip/zlib 압축 및 base64 스테이징 변형(압축 해제/디코딩을 수행함; picklescan의 파일 경로 항목은 그렇지 않음)과 numpy-object 케이스입니다. 위장된 파일의 경우, 현재 picklescan도 내용을 스니핑하여 포착하지만, 명시적으로
실제로 작동하는 방어 기법들
-
safetensors를 선호하세요, 무조건입니다. safetensors는 실행 가능한 opcodes가 없는 텐서를 저장합니다. 만약 리포지토리가 둘 다 제공한다면, safetensors를 로드하고 pickle은 절대 건드리지 마세요.
-
CI에서 로드하기 전에 스캔하세요. 모델이 빌드 이미지에 진입하기 전에
airlock scan model hf:org/name --fail-on high를 실행하세요.torch.load()후에 실행되는 스캐너는 사후 분석(post-mortem)일 뿐입니다. -
고보안 환경에서는 allowlist 모드를 켜세요.
--strict는 denylist가 잡아내지 못하는 새로운 임포트를 포착합니다. 실제 가중치(weights)는 작은 모듈 세트에서 가져오기 때문에, 이 경우 오탐(false-positive) 비용은 거의 없습니다. -
자체 도구링에서는 확장자를 절대 신뢰하지 마세요. 모델 처리 코드를 구축한다면, 매직 바이트(magic bytes)를 스니핑하세요. 2025년 CVE들은 이 방식을 무시했을 때 어떤 일이 벌어지는지 보여주는 기념비적인 사례입니다.
-
피할 수 없는 것은 샌드박싱 하세요. 만약 신뢰할 수 없는 pickle을 반드시 로드해야 한다면, 네트워크 연결이 없고 비밀 정보(secrets)가 마운트되지 않은 격리된 컨테이너에서 실행하세요.
핵심 의견:
-
JFrog — 세 가지 제로데이 PickleScan 취약점 — CVE-2025-10155 확장 우회 설명.
-
Sonatype — PickleScan 우회 (4가지 추가 취약점) — 2025년 12월 라운드.
-
Trail of Bits — Fickling의 allowlist pickle 스캐너 — denylist보다 allowlist가 우세한 이유, 소스 코드에서.
-
Cisco — 구조 인식 퍼징(structure-aware fuzzing)으로 pickle 스캐너 강화 — 기존 업체들이 어떻게 강화되었는지.
-
Python
pickletools문서 — 정적 분석을 가능하게 하는 디어셈블러.
AI 자동 생성 콘텐츠
본 콘텐츠는 Dev.to AI tag의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.
원문 바로가기