오늘 COLDCARD로 만든 지갑에서 1억 달러가 넘는 비트코인이 털렸다는 소식을 봤습니다. 저는 콜드월렛을 쓰지 않는데도 “이게 어떻게 가능하지?” 싶었습니다.
“인터넷에서 분리해 둔 콜드월렛을 대체 어떻게 해킹한 거지?”
처음에는 누군가 기기를 원격으로 뚫었거나 제조 과정에 백도어를 심은 줄 알았습니다. 실제 공격은 전혀 다른 곳을 노렸습니다. 콜드월렛에 접속하지 않았고, 비트코인 네트워크도 해킹하지 않았습니다.
문제는 지갑이 처음 만든 열쇠였습니다. 그 열쇠 자체가 약했습니다.
일부 COLDCARD 펌웨어가 암호학적으로 안전한 하드웨어 난수 대신 예측 가능한 소프트웨어 난수로 시드를 만들었습니다. 공격자는 가능한 시드를 자기 컴퓨터에서 계산해 공개 블록체인 주소와 대조했고, 일치하는 개인키로 자금을 옮긴 것으로 분석됐습니다.

콜드월렛이 인터넷에서 뚫린 것이 아닙니다. 처음 생성한 시드의 후보 범위가 좁아 공격자가 공개 주소와 대조해 개인키를 찾을 수 있었습니다.
먼저 핵심부터 정리하면
- 2021년 3월부터 일부 COLDCARD 펌웨어가 시드 생성에 예측 가능한 소프트웨어 난수 경로를 사용했습니다.
- 공격자는 기기나 시드 문구를 훔치지 않고도 후보 키를 계산해 공개 블록체인 주소와 비교할 수 있었습니다.
- 첫 대규모 공격은 공식 공개 약 30시간 전에 시작됐고, 8월 4일 기준 확인된 피해는 약 1,596 BTC로 집계됐습니다.
- 펌웨어 업데이트는 앞으로 만들 시드만 고칩니다. 이미 약한 난수로 만들어진 시드는 새 시드로 자금을 옮겨야 합니다.
- 비트코인이나 콜드월렛이라는 개념 전체가 깨진 사건은 아닙니다. COLDCARD 펌웨어의 키 생성 구현이 실패한 사건입니다.
콜드월렛 안에는 비트코인이 들어 있지 않습니다
콜드월렛을 비트코인이 담긴 USB 금고처럼 생각하기 쉽지만, 실제 구조는 다릅니다.
비트코인은 블록체인 장부에 기록됩니다. 콜드월렛이 보관하는 것은 그 비트코인을 움직이고 거래에 서명하는 개인키입니다. 이 개인키를 인터넷과 떨어진 곳에 두는 장치가 콜드월렛입니다.
하나의 시드에서 키와 주소가 다음 순서로 만들어집니다.
시드 문구 -> 개인키 -> 공개 주소 -> 거래 서명
시드 문구는 지갑 전체를 복구하는 출발점입니다. 지갑을 처음 설정할 때 적어두는 12개나 24개 단어가 바로 시드 문구입니다. 여기에서 개인키와 주소가 정해진 규칙에 따라 계속 파생됩니다.
시드가 충분히 무작위라면 공격자가 개인키를 맞히는 것은 현실적으로 불가능합니다. 하지만 난수가 약하면 상황이 달라집니다. 금고 문은 두꺼운데 열쇠 번호가 몇 가지 안 되는 셈입니다.
정상적인 시드는 왜 추측할 수 없을까요?
12단어 BIP-39 시드는 일반적으로 128비트 엔트로피에서 만들어집니다. 가능한 경우의 수는 $2^{128}$개입니다.
2^128 = 340,282,366,920,938,463,463,374,607,431,768,211,456
컴퓨터를 아무리 많이 동원해도 모든 후보를 하나씩 훑어서는 정답에 도달할 수 없는 규모입니다.
그래서 하드웨어 지갑에는 암호학적으로 안전한 난수 생성기가 중요합니다. 장치 안의 하드웨어 RNG가 예측할 수 없는 값을 만들고, 그 값을 이용해 시드를 생성해야 합니다.
COLDCARD도 원래는 이렇게 동작해야 했습니다.
그런데 펌웨어가 다른 난수 생성기를 불렀습니다
Block 연구진이 코드를 추적해 보니 펌웨어를 합치는 과정에서 문제가 생겼습니다.
COLDCARD는 자체 하드웨어 RNG 래퍼를 따로 만들었고, 그 때문에 MicroPython의 기본 하드웨어 RNG를 설정에서 꺼뒀습니다.
#define MICROPY_HW_ENABLE_RNG (0)
문제는 libngu 라이브러리의 검사 방식이었습니다. 이 코드는 값이 0인지 확인하지 않고, 매크로가 존재하는지만 검사했습니다.
#ifndef MICROPY_HW_ENABLE_RNG
#error "get a HW TRNG plz"
#endif
MICROPY_HW_ENABLE_RNG는 값이 0이어도 이미 정의된 상태입니다. #ifndef 검사는 이 차이를 구분하지 못하고 통과했습니다.
그래서 ngu.random은 COLDCARD가 따로 만든 하드웨어 RNG가 아니라 MicroPython의 rng_get()으로 연결됐습니다. 하드웨어 기능이 꺼진 rng_get()은 Yasmarang이라는 결정론적 소프트웨어 난수 생성기를 사용했습니다.
결정론적이라는 건 시작 조건이 같으면 똑같은 숫자 흐름이 나온다는 뜻입니다.
Yasmarang은 장치의 마이크로컨트롤러 UID와 시스템 타이머 값 등으로 초기화됐습니다. 둘 다 무작위로 뽑은 비밀이 아닙니다. 범위가 좁혀지면 공격자는 같은 조건을 재현해 출력 후보를 다시 만들 수 있습니다.
코드 한 줄이 암호를 직접 유출한 것이 아닙니다. 안전한 난수 생성기로 가야 할 호출이 예측 가능한 대체 경로로 연결된 것이 문제였습니다.
공격자는 공개 블록체인을 정답지로 썼습니다
약한 난수의 구조를 안다고 바로 특정 사용자의 시드를 얻는 것은 아닙니다. 수많은 후보 가운데 실제 비트코인이 들어 있는 지갑을 골라내야 합니다.
이때 공개 블록체인이 정답지 역할을 했습니다.
기술 분석에서 추정한 공격 순서는 이렇습니다.
- 먼저 장치 UID와 부팅 시점, 타이머 상태, 난수 호출 횟수의 범위를 좁힙니다.
- 가능한 조건마다 시드를 대량으로 생성합니다.
- 시드 하나마다 개인키와 비트코인 주소를 계산합니다.
- 계산한 주소를 공개 블록체인에 있는 주소와 대조합니다.
- 잔액이 있는 주소가 나오면 시드 후보를 다시 검증합니다.
- 맞는 개인키를 찾으면 거래에 서명해 공격자 주소로 비트코인을 옮깁니다.
공개 주소는 누구나 볼 수 있지만 원래는 개인키를 알아내는 데 도움이 되지 않습니다. 시드가 128비트 무작위라 후보 공간이 너무 크기 때문입니다.
이번에는 처음부터 난수 범위가 좁았습니다. 후보 수가 현실적인 규모로 줄자 공개 블록체인이 빠른 검증 수단으로 바뀌었습니다.
공격자는 콜드월렛에 손댈 필요가 없었습니다. 시드 문구를 피싱하거나 장치를 인터넷에 연결할 이유도 없었습니다. 자기 컴퓨터에서 후보를 만들고 블록체인과 대조하는 것으로 개인키를 찾을 수 있었다는 분석입니다.
피해는 얼마나 컸을까요?
피해 수치는 조사 과정에서 계속 늘었습니다. Galaxy Research를 인용한 보도 기준으로 피해 규모는 다음처럼 변했습니다.
| 시점 | 확인된 피해 | 주소 수 | 비고 |
|---|---|---|---|
| 7월 30일 첫 공격 | 약 1,083 BTC | 1,196개 | 약 41분 동안 진행 |
| 8월 1일 3개 파동 합계 | 1,367 BTC | 4,585개 | 당시 약 8,860만 달러 |
| 8월 4일 확인치 | 1,596 BTC | 약 7,300개 | 1억 달러 초과 |
| 의심되는 4차 공격 포함 | 최대 2,055 BTC | 조사 중 | 약 1억 3천만 달러 추정 |
마지막 2,055 BTC는 확인이 끝난 피해액이 아니라 의심되는 추가 파동까지 포함한 추정치입니다. 확정 수치처럼 받아들이면 안 됩니다.
첫 공격에는 특이한 공통점이 있었습니다. 41분 동안 발생한 거래가 모두 30 sat/vB라는 동일한 수수료율을 썼고 거스름돈 출력을 남기지 않았습니다. 당시 일반 수수료보다 훨씬 높은 값이었습니다.
Galaxy Research는 이 패턴을 사용자가 직접 자금을 옮긴 흔적이 아니라, 이미 확보한 키를 자동화 도구로 빠르게 쓸어간 흔적으로 봤습니다. 첫 대규모 탈취가 Coinkite의 공개 보안 권고보다 약 30시간 먼저 시작됐다는 점도 눈에 걸립니다.
에어갭은 왜 막아주지 못했을까요?
에어갭은 장치와 인터넷 사이의 통신 경로를 끊습니다. 악성코드가 네트워크로 개인키를 빼내거나 원격으로 거래를 승인할 위험을 줄여줍니다.
하지만 에어갭은 잘못 만들어진 키를 강한 키로 바꿔주지 않습니다.
이번 문제는 통신보다 앞선 키 생성 단계에서 이미 생겼습니다.
키 생성 오류 -> 약한 시드 -> 개인키 후보 복원 -> 공격자 서명
공격자는 콜드월렛과 통신할 필요가 없었습니다. 블록체인에 공개된 주소와 자기 컴퓨터에서 만든 후보만 있으면 됐습니다.
이번 사건을 “에어갭이 뚫렸다"고 표현하면 핵심을 놓치게 됩니다. 에어갭 안에서 만든 비밀 자체가 처음부터 약했습니다.
오픈소스인데 왜 5년 동안 못 찾았을까요?
COLDCARD는 검증 가능한 오픈소스 펌웨어를 장점으로 내세워 왔습니다. 실제 코드가 공개돼 있었는데도 2021년 3월에 들어간 문제가 2026년까지 남았습니다.
저는 이 대목이 가장 흥미로웠습니다.
소스 공개가 곧 검증 완료를 뜻하지는 않습니다. 이번 버그도 한 파일만 들여다봐서는 쉽게 드러나지 않았습니다.
COLDCARD 보드 설정은 하드웨어 RNG 기능을 0으로 정의했고, 별도 RNG 함수는 다른 이름으로 구현했습니다. 그런데 libngu는 매크로 값이 아니라 존재 여부만 확인했습니다.
MicroPython은 같은 매크로의 값을 보고 소프트웨어 대체 경로를 컴파일했습니다. 난수 상태 검사도 연속된 값이 서로 다른지만 확인했기 때문에 결정론적 PRNG를 걸러내지 못했습니다.
코드는 문제없이 빌드됐고 장치도 겉보기에는 정상 작동했습니다. 시드 문구 역시 그럴듯한 단어로 나왔습니다. 사용자가 화면만 보고 난수가 약하다는 사실을 알아낼 방법은 없었습니다.
소스 공개는 검증할 기회를 줍니다. 하지만 실제 빌드와 하드웨어 경계까지 시험하지 않으면 공개된 버그가 그대로 남을 수 있습니다.
모든 COLDCARD가 같은 위험이었던 것은 아닙니다
Coinkite의 8월 1일 공식 보안 권고 기준으로 영향 범위는 시드를 생성할 당시의 모델과 펌웨어에 따라 달라집니다.
| 모델 | 영향받는 시드 생성 버전 | 수정 버전 |
|---|---|---|
| Mk2, Mk3 | 4.0.1부터 4.1.9까지 | 4.2.0 이상 |
| Mk4, Mk5 표준 | 5.6.0 이전 | 5.6.0 이상 |
| Q 표준 | 1.5.0Q 이전 | 1.5.0Q 이상 |
| Mk4, Mk5 Edge | 6.6.0X 이전 | 6.6.0X 이상 |
| Q Edge | 6.6.0QX 이전 | 6.6.0QX 이상 |
TAPSIGNER, OPENDIME, SATSCARD는 코드 기반이 달라 이번 버그의 영향을 받지 않는다고 발표했습니다.
시드를 만들 때 독립적이고 비공개인 주사위 입력을 50회 이상 추가했다면 이번 RNG 문제만으로는 위험하지 않다는 설명도 나왔습니다. 주사위에서만 128비트 이상의 엔트로피를 얻을 수 있기 때문입니다.
강력하고 고유한 BIP-39 패스프레이즈도 공격자가 넘어야 할 장벽이 됩니다. 하지만 짧거나 흔한 문구는 추측할 수 있고, 패스프레이즈를 붙여도 약해진 시드 자체가 고쳐지지는 않습니다. Coinkite는 이 경우에도 새 시드로 옮기라고 권고했습니다.
펌웨어만 올리면 끝나는 문제가 아닙니다
일반적인 소프트웨어 업데이트와는 다릅니다.
업데이트가 보호하는 건 새로 만들 시드뿐입니다. 이미 만들어진 시드는 약한 후보 공간 안에 그대로 남습니다. 새 펌웨어를 설치하고 같은 시드를 복원해도 시드 값은 바뀌지 않습니다.
Coinkite가 권고한 이동 순서는 이렇습니다.
- 해당 모델의 수정 펌웨어를 설치하고 버전을 확인합니다.
- 수정된 장치에서 완전히 새로운 시드를 생성합니다.
- 백업과 지갑 지문을 기록하고 새 수신 주소를 장치 화면에서 확인합니다.
- 먼저 소액을 시험 전송합니다.
- 새 지갑에서 도착을 확인한 뒤 나머지 자금을 옮깁니다.
- 전체 잔액과 확인이 끝날 때까지 기존 백업을 보관합니다.
Coinkite는 영향을 받은 장치를 버리지 말라고 안내했습니다. 자금 회수나 수사 과정에서 필요할 수 있다는 이유입니다.
그렇다면 비트코인이 해킹된 걸까요?
아닙니다. 비트코인, COLDCARD, 사용자 키라는 세 층을 나눠 봐야 합니다.
| 구분 | 이번 사건에서 일어난 일 |
|---|---|
| 비트코인 프로토콜 | 깨지지 않았습니다 |
| 블록체인 서명 방식 | 깨지지 않았습니다 |
| COLDCARD 펌웨어 | 시드 생성 난수 경로가 잘못됐습니다 |
| 사용자 개인키 | 약한 시드에서 복원됐을 가능성이 큽니다 |
공격자는 비트코인 암호를 수학적으로 풀지 않았습니다. 약한 시드에서 개인키를 찾아낸 뒤 그 키로 정상 서명을 만들었습니다. 블록체인에서는 진짜 소유자가 보낸 거래와 구별할 방법이 없습니다.
비트코인 자체의 결함이라면 네트워크 전체가 위험해집니다. 이번 사건의 원인은 특정 하드웨어 지갑과 그 펌웨어로 만든 시드에 있습니다.
콜드월렛이 무의미해진 사건도 아닙니다
이번 사고를 보고 “그럼 콜드월렛도 필요 없네"라고 결론 내리는 것도 과합니다.
하드웨어 지갑은 여전히 개인키를 일상적으로 쓰는 컴퓨터와 분리해 줍니다. 거래 내용을 별도 화면에서 확인할 수 있고, 원격 악성코드가 노릴 수 있는 범위도 줄어듭니다. 이 장점까지 사라진 것은 아닙니다.
다만 자기보관에도 믿고 맡겨야 하는 지점은 남습니다.
- 제조사가 난수를 올바르게 구현했는가
- 실제 펌웨어가 공개된 소스와 같은가
- 키 생성 경로가 독립적으로 시험됐는가
- 보안 칩과 펌웨어 경계에서 엔트로피가 줄지 않는가
- 하나의 장치나 하나의 제조사 실패가 전체 자금으로 번지지 않는가
큰 금액을 보관할 때 서로 다른 제조사의 장치로 멀티시그를 구성하면 한 제품의 구현 실패에 덜 의존할 수 있습니다. 대신 백업과 복구가 훨씬 복잡해지고, 장치를 늘릴수록 운영 실수도 늘어날 수 있습니다.
정리하면
이번 사건에서 뚫린 것은 인터넷 연결이 아니었습니다.
COLDCARD 펌웨어가 안전한 하드웨어 난수 대신 예측 가능한 소프트웨어 난수로 시드를 만들었습니다. 공격자는 그 시드에서 나온 주소를 공개 블록체인과 대조해 개인키를 찾아낸 것으로 분석됩니다.
금고를 부순 것이 아니라, 공장에서 만든 열쇠의 후보가 너무 적어서 같은 열쇠를 다시 찾아낸 사건입니다.
제가 콜드월렛을 쓰지 않는데도 이 사건이 흥미로웠던 이유가 여기에 있습니다. 에어갭과 보안 칩, 오픈소스를 모두 갖춰도 가장 아래쪽의 난수 생성이 틀리면 소용이 없었습니다.
보안 기능을 여러 겹 쌓았다고 시스템이 저절로 안전해지는 것은 아닙니다. 가장 약한 경로 하나가 전체 강도를 결정한다는 오래된 원칙을, 1억 달러가 넘는 피해로 다시 확인한 사건이었습니다.
댓글