Back to Blog

COLDCARD 사건: 지갑의 "무작위" 시드가 무작위가 아니었을 때

Code Auditing
August 7, 2026
14 min read
Key Insights
  • COLDCARD의 손실은 단일 빌드 구성 오류에서 비롯됩니다 — 매크로 가드가 값(#if) 대신 존재 여부(#ifndef)를 확인하여, 시드 생성이 결정론적 소프트웨어 RNG 폴백으로 조용히 라우팅되었습니다.

  • 공격은 완전히 오프라인으로 이루어졌습니다: 온체인 익스플로잇 트랜잭션은 없었으며, 공개 지갑 데이터와 대조하여 시드를 열거(Mk2/Mk3에서 약 40비트, Mk4/Q/Mk5에서 약 72비트)하는 방식이었습니다.

  • 펌웨어를 업데이트해도 이미 생성된 시드는 수정되지 않습니다 — 손상된 자금은 패치된 릴리스에서 생성된 새 시드로 이동해야 합니다.

  • 이 사례는 암호화 엔트로피 경로가 컴파일 시점뿐만 아니라 출하된 펌웨어에서 종단 간 검증되어야 하며, 실패 시 반드시 닫혀야(fail closed) 하는 이유를 잘 보여줍니다.

간략한 요약

2026년 7월 30일부터 COLDCARD 비트코인 하드웨어 지갑에서 여러 차례에 걸쳐 자금이 탈취되기 시작했으며, 이후 며칠간 계속해서 추가 피해가 발생했다. 단일 온체인 공격 트랜잭션이나 취약한 컨트랙트는 존재하지 않았으며, 피해는 오프라인 시드 복구 문제에 이어 발생한 온체인 스윕에서 비롯되었다. 2026년 8월 7일 기준으로, 온체인 추적을 통해 약 4,925개 주소에서 최소 1,405 BTC (8월 7일 가격 $64,700 기준 약 $91M)가 탈취된 것이 확인되었으며 [1], 웨이브 단위 귀속 분석을 통해 10개 웨이브에 걸쳐 약 1,433 BTC가 집계되었고 [2], 피해자와의 비공개 채널 대조 결과는 최대 2,055 BTC (약 $133M)에 달하는 것으로 나타났다 [3].

근본 원인은 지갑 엔트로피 오류였다. 2021년 펌웨어 마이그레이션 과정에서 시드 생성 경로가 ngu.random.bytes()를 통하도록 변경되었는데, 이 함수가 의도한 STM32 하드웨어 RNG 대신 결정론적 소프트웨어 생성기로 폴백하게 된 것이다. 영향을 받은 장치에서는 시드 복구 문제가 암호학적으로 불가능한 문제에서 오프라인 탐색 문제로 축소되어, 공격자가 후보 시드를 열거하고 공개 지갑 데이터와 대조하여 개인 키를 복구할 수 있게 되었다. 이번 사건에 대한 이전 주간 분석을 바탕으로, 이 심층 분석에서는 영향을 받은 각 장치 세대별 잔여 엔트로피를 정량화하고, 피해자 및 탈취 자금이 온체인에서 어떻게 식별되었는지 추적하며, 핫픽스 이후 발생한 로그인 전 서비스 거부를 유발할 수 있는 별도의 펌웨어 회귀 문제도 살펴본다.

배경

COLDCARD는 비트코인 하드웨어 지갑이다. 다른 자기 보관 지갑과 마찬가지로, 보안은 궁극적으로 지갑 설정 시 생성되는 시드 구문의 예측 불가능성에 달려 있다. BIP-39 시드 구문은 사람이 읽을 수 있는 형태이지만, 기반이 되는 보안 속성은 여전히 시드를 생성하는 데 사용된 임의 바이트의 엔트로피이다. 시드 생성 출력이 예측 가능하다면, 이후 시드를 아무리 신중하게 보관하더라도 지갑의 보안은 무너진다.

COLDCARD는 Mk1부터 Mk5까지의 하드웨어 리비전과 Q 모델을 아우른다. Mk2와 Mk3는 레거시 펌웨어 라인을 사용한다. Mk4, Mk5, Q는 별도의 Standard 및 Edge 릴리스 트랙을 사용한다.

대부분의 COLDCARD 애플리케이션 로직은 MicroPython 위에서 실행되는 Python으로 작성되어 있으며, 하드웨어 및 암호화 연산은 네이티브 C 모듈이 담당한다. 안전한 시드 생성 경로는 STM32 하드웨어 RNG에서 32바이트를 읽고, 주변 장치가 멈추거나 값을 반복할 경우 실패하며, 결과를 해시한 후 BIP-39 단어로 인코딩해야 한다. v3.2.2에서 make_new_wallet()은 다음 경로를 따랐다:

make_new_wallet()
  |- ckcc.rng_bytes(seed)
  |    `- random_buffer()
  |         |- STM32 RNG->DR 읽기
  |         `- 타임아웃 또는 반복 출력 시 실패
  |- SHA-256(seed)
  `- BIP-39 시드 단어

취약점 분석

근본 원인은 MICROPY_HW_ENABLE_RNG와 관련된 빌드 및 통합 오류였다. COLDCARD의 프로덕션 보드 설정은 이 매크로를 0으로 설정했는데, 펌웨어가 MicroPython의 하드웨어 RNG 구현 대신 자체 보드 로컬 하드웨어 RNG 래퍼를 사용했기 때문이다. 그러나 지갑 생성 경로는 ngu.random.bytes(32)로 마이그레이션되었고, libngu의 STM32 경로는 궁극적으로 전역 rng_get() 심볼에 의존했다.

영향을 받은 경로는 libngu의 my_random_bytes()로 진입했다. 각 출력 워드에 대해, CHIP_TRNG_32()가 반환한 값과 자체 Yasmarang 출력을 XOR했다:

generate_seed()
  `- ngu.random.bytes(32)
       `- my_random_bytes()                [libngu]
            |- CHIP_TRNG_32()
            |    `- rng_get()              [MicroPython]
            |         `- Yasmarang A
            |              `- UID + SysTick + RTC
            |
            |- my_yasmarang()              [libngu]
            |    `- Yasmarang B
            |         |- Mk2/Mk3: 공개 초기 상태
            |         `- Mk4/Q/Mk5: 32비트 패드 리시드
            |
            `- 출력 = Yasmarang A XOR Yasmarang B

Yasmarang A는 rng_get() 뒤에 있는 MicroPython의 폴백으로, 장치 및 타이머 상태로 초기화되었다. Yasmarang B는 libngu에 속했다: Mk2/Mk3에서는 공개 초기값을 사용했고, Mk4/Q/Mk5에서는 32비트 pad만 시큐어 엘리먼트에서 파생된 데이터로 교체했다. 보드 로컬 STM32 RNG는 여전히 존재했지만, ngu.random.bytes()는 이를 호출하지 않았다.

불일치는 빌드 설정에서 직접 확인할 수 있다. Mk4 mpconfigboard.h는 MicroPython의 하드웨어 RNG 브랜치를 비활성화했다:

// 이 코드의 자체 버전을 보유하고 있습니다.
#define MICROPY_HW_ENABLE_RNG (0)

Libngu의 CHIP_TRNG_32()는 매크로가 존재하는지만 확인한 후 rng_get()을 호출했기 때문에, 이 매크로를 하드웨어 RNG의 충분한 증거로 여전히 간주했다:

extern uint32_t rng_get(void);
#define CHIP_TRNG_32() rng_get()

#ifndef MICROPY_HW_ENABLE_RNG
#error "HW TRNG를 사용하세요"
#endif

이 가드는 위험한 경우를 놓친다: 0으로 정의된 매크로도 여전히 정의된 것으로 간주된다. 따라서 빌드는 성공했고, rng_get()은 COLDCARD의 별도 보드 로컬 래퍼(random32() / random_buffer(), Python에는 ckcc.rng_bytes로 노출됨) 대신 MicroPython의 STM32 RNG 모듈로 해석되었다. MicroPython의 rng_get() 선택에서, MICROPY_HW_ENABLE_RNG == 0은 소프트웨어 폴백 브랜치를 선택했다:

#if MICROPY_HW_ENABLE_RNG
    // STM32 하드웨어 RNG
#else
    // Yasmarang 소프트웨어 폴백
#endif

Mk2/Mk3: 약 40비트

컴파일된 pyb_rng_yasmarang() 폴백은 Yasmarang을 다음과 같이 초기화하고 진행했다:

STATIC uint32_t pyb_rng_yasmarang(void) {
    static bool seeded = false;
    static uint32_t pad = 0, n = 0, d = 0;
    static uint8_t dat = 0;

    if (!seeded) {
        seeded = true;
        rtc_init_finalise();
        pad = *(uint32_t *)MP_HAL_UNIQUE_ID_ADDRESS ^ SysTick->VAL;
        n = RTC->TR;
        d = RTC->SSR;
    }

    pad += dat + d * n;
    pad = (pad << 3) + (pad >> 29);
    n = pad | 2;
    d ^= (pad << 31) + (pad >> 1);
    dat ^= (char)pad ^ (d >> 8) ^ 1;

    return pad ^ (d << 5) ^ (pad >> 18) ^ (dat << 1);
}

변수들은 104비트를 차지하지만, 상태 크기가 엔트로피를 의미하지는 않는다. dat는 0에서 시작하고, 나머지 값들은 독립적인 비밀이 아닌 고정 메타데이터이거나 상관관계가 있는 타이머 읽기값이다. 약 40비트 수치에 사용된 느슨한 Mk2/Mk3 모델에서:

입력 후보 값 수 열거 비용
알려진 UID_low32 1 2^0
SysTick->VAL 80,000 2^16.29
RTC->TR 하루 중 시각 86,400 2^16.40
RTC->SSR 서브초 256 2^8

모든 타이머 필드를 독립적으로 취급하면 의도적으로 넓은 상한이 나온다:

80,000 * 86,400 * 256
= 1,769,472,000,000
= 2^40.69 후보 초기 상태

따라서 완전 탐색은 최대 2^40.69번의 시도를 필요로 하며, 균일 위치 가정 하에 평균 약 2^39.69번의 시도가 필요하다. 이는 열거 상한이지, 40비트의 암호학적 엔트로피가 아니다. 정상 콜드 부트 중 RTC 레지스터가 정적이라면 SysTick만 남아 상한이 약 2^16.29로 줄어든다. UID, 타이머, 이전 RNG 호출 횟수를 알고 있다면 스트림은 정확히 하나, 즉 2^0이다. 알 수 없는 호출 이력은 새로운 엔트로피 소스가 아닌 그럴듯한 실행 경로 수만 추가할 뿐이다. 마찬가지로, 256비트 시드를 위해 32비트 워드 8개를 생성해도 탐색 공간이 곱해지지 않는다: 모든 워드는 동일한 초기 상태에 의해 결정되기 때문이다.

libngu 레이어에서, my_random_bytes()는 위의 MicroPython 폴백과 libngu의 별도 Yasmarang 생성기를 혼합했다. 소스에는 문자 그대로 chip = rng_get()이 포함되지 않는다: CHIP_TRNG_32()rng_get()으로 확장된다. Mk2/Mk3에서 두 번째 생성기는 공개 상수에서 시작했다:

static uint32_t yasmarang_pad = 0x0a8ce26f, yasmarang_n = 69, yasmarang_d = 233;
static uint8_t yasmarang_dat = 0;

uint32_t chip = CHIP_TRNG_32();
// ... 인접 출력 상태 검사 ...
chip ^= my_yasmarang();

따라서 MicroPython 폴백 상태와 호출 이력을 알면 두 스트림 모두 재현 가능했다. XOR은 출력값을 변경했지만 엔트로피를 추가하지 않았다. UID_low32를 모르더라도, UID_low32 ^ SysTick은 두 입력을 하나의 32비트 pad로 축소하므로 명목상의 비트 수를 더할 수 없다.

Mk4/Q/Mk5: 약 72비트

후속 모델들은 동일한 두 생성기 구조를 유지했지만, rng_seeding()이 부팅 시 libngu의 생성기에 시큐어 엘리먼트 데이터를 추가했다:

a = callgate.read_rng(1)       # SE1
b = callgate.read_rng(2)       # SE2

n = ngu.hash.sha256d(a + b)
n, = ustruct.unpack('I', n[0:4])
ngu.random.reseed(n)

40바이트가 해시에 입력되었지만, 처음 4바이트만 reseed()에 전달되었다 [4]. random_reseed() 구현은 libngu의 32비트 pad 워드만 교체했다:

STATIC mp_obj_t random_reseed(mp_obj_t arg)
{
    yasmarang_pad = mp_obj_get_int_truncated(arg);
    return mp_const_none;
}

나머지 libngu 상태 워드들은 공개 값을 유지했고, MicroPython 폴백은 리시드되지 않았다. 약 72비트 수치는 32비트 libngu 리시드와 후속 모델의 타이머 상태에 대한 느슨한 상한을 결합한 것이다:

MicroPython 폴백:
    120,000 SysTick 값 * 86,400 RTC 시각 * 256 서브초
    = 2^41.27 상태

Libngu 시큐어 리시드:
    2^32 값

결합된 상한:
    2^41.27 * 2^32 = 2^73.27 후보

평균 열거:
    2^73.27 / 2 = 2^72.27 시도

이것이 "약 72비트" 수치의 출처이다. 이는 시큐어 엘리먼트가 제공하는 72비트가 아닌, 평균 공격 작업량 추정치이다. 타이머 필드들은 상관관계가 있으며 재구성될 수 있다; MicroPython 폴백 상태를 알고 있다면 2^32 리시드 값만 남아 평균 2^31번의 시도가 필요하다. 최종 32개 임의 바이트를 해싱해도 가능한 시드 수는 증가하지 않는다.

영향을 받은 버전

장치 및 트랙 이 회귀 외부 영향을 받은 시드 생성 펌웨어 실효 비트 보안 최초 수정 릴리스
Mk1 v3.0.6까지 없음 - 해당 없음
Mk2/Mk3 v3.2.2까지 v4.0.0-v4.1.9 (공식 권고는 v4.0.1부터) 영향 받은 경우 약 40비트 v4.2.0
Mk4/Mk5 Standard 해당 없음 v5.6.0 이전 수정 전 약 72비트; 수정 후 최소 128비트 v5.6.0
Q Standard 해당 없음 v1.5.0Q 이전 수정 전 약 72비트; 수정 후 최소 128비트 v1.5.0Q
Mk4/Mk5 Edge 해당 없음 v6.6.0X 이전 수정 전 약 72비트; 수정 후 최소 128비트 v6.6.0X
Q Edge 해당 없음 v6.6.0QX 이전 수정 전 약 72비트; 수정 후 최소 128비트 v6.6.0QX

관련 버전은 현재 설치된 펌웨어가 아니라 시드를 생성한 펌웨어이다. 수정된 릴리스 이후에 생성된 새 시드는 수정된 경로를 사용하지만, 업데이트해도 기존 시드는 복구되지 않는다. Mk2/Mk3의 공식 영향 범위는 v4.0.1부터 시작하며 [5], 소스 레벨 분석에는 v4.0.0도 포함된다 [4]. 독립적인 주사위 엔트로피는 시드의 보안을 높일 수 있으며, 강력한 BIP-39 패스프레이즈는 시드 자체를 복구하지 않고도 별도의 장벽을 추가한다 [6].

Web3 최고의 보안 감사 기관

출시 전 설계, 코드, 비즈니스 로직을 검증하세요

피해자 및 자금 추적

추적할 온체인 익스플로잇 트랜잭션이 없었기 때문에, 복구는 필연적으로 오프라인에서 이루어져야 했다. 영향을 받은 펌웨어로 생성된 시드를 대상으로, 공격자는 위에서 설명한 후보 RNG 상태를 제한하고 열거하고, 각 후보의 시드 생성 스트림을 재현하고, 결과 지갑 키를 파생하여 공개 지갑 데이터와 대조한 후, 일치하는 자금이 있는 지갑을 온체인에서 스윕할 수 있었다. 이는 시드만으로 파생 가능한 지갑에만 해당했다: 강력하고 고유한 BIP-39 패스프레이즈는 RNG 결함이 결코 건드리지 않은 독립적인 사용자 제공 엔트로피를 PBKDF2를 통한 키 파생에 혼합함으로써, 순수 시드 열거의 범위 밖에 해당 지갑을 두었다. 스윕된 지갑들은 필연적으로 그러한 보호가 없었던 것들이었다 [6]. 탈취가 추적 가능한 익스플로잇이 아닌 온체인 스윕으로만 표면화되었기 때문에, 피해자를 식별하고 자금을 추적하는 것은 온체인 포렌식의 문제가 되었다.

여러 독립적인 노력이 탈취된 자금을 추적했다: 공개 추적 사이트(Coldcard Sweep Watch [1], coldcard.rip [2], Coldcard Hack Tracker [7])와 Galaxy Research의 비공개 채널 대조 [3]가 있으며, 보고된 총액은 아래에서 비교된다. Coldcard Sweep Watch가 방법론을 공개했기 때문에, 이를 식별 프로세스 설명에 활용한다. 이는 오프체인 보고와 온체인 분석 간의 피드백 루프이다:

  1. 오프체인 앵커. 피해자와 연구자들이 공개 주소 또는 트랜잭션 ID와 함께, 가능한 경우 장치 및 시드 생성 컨텍스트를 제공했다. 각 보고는 리드로 처리되어 온체인에서 확인되었으며, 시드 구문, 개인 키, xpub은 필요하지 않았다.
  2. 온체인 확장. 확인된 앵커에서 시작하여, 스캐너가 동일한 스윕 특성을 가진 관련 블록을 검색했다: 잔돈 없이 비워진 지갑, 유사한 입력 유형, 촘촘한 타이밍, 반복된 수수료율, 공통 목적지, 또는 이후 공동 지출.
  3. 오프체인 교차 검증. 새로운 피해자 보고, 연구자 데이터셋, 서비스 귀속을 통해 후보 웨이브를 확인하거나 기각했다. 검증된 클러스터와 휴리스틱 후보는 별도로 유지되었다.

비트코인은 사람이나 지갑 모델이 아닌 주소를 식별한다. 하나의 지갑이 여러 주소를 제어할 수 있으므로, 주소 수는 피해자 수가 아니다. 처음 널리 보고된 주요 웨이브인 960188594.48 BTC를 차지했으며, 계속된 스캔과 보고로 총액이 증가했다. 2026년 8월 7일 기준으로, Coldcard Sweep Watch는 약 4,925개 주소에서 최소 약 1,405.07 BTC (8월 7일 $64,700 가격 기준 약 $91M)가 확인되었다고 보고했으며 [1], 8월 3일 coldcard.rip 스냅샷은 5,477개 주소에 걸친 10개 웨이브에서 최대 1,433.13 BTC 총액, 수수료 후 목적지 기준 1,432.48 BTC를 귀속시켰다 [2]. 피해자와의 서신을 바탕으로 한 Galaxy Research의 별도 비공개 채널 대조는 약 1,596 BTC에서 최대 2,055 BTC (같은 가격 기준 약 $133M)로 더 높은 수치를 제시했다 [3]. 차이는 증거 기준, 발견 시점, 각 추적기가 의존하는 확인 채널에 따른 것이다.

자금은 이후 세 개의 관찰된 레이어를 통해 추적되었다: 스윕된 소스 주소, 직접 스윕 목적지(holding), 이후 통합 목적지(vault). 표는 각 레이어의 고유 주소 수를 보여준다:

패턴 예시 경로 추적 결과
여러 스윕이 하나 또는 두 개의 홀딩 주소로, 그 후 하나의 볼트로 960183: 204 -> 2 -> 1; 960188: 500 -> 1 -> 1 목적지 수렴으로 클러스터가 비교적 강력하고 추적하기 쉬움
스윕이 이후 통합 없이 홀딩 주소에서 멈춤 960352: 352 -> 1 -> 0; 960668: 795 -> 1 -> 0 홀딩 주소는 추적 가능하지만, 이후 공동 지출이 없어 귀속 강도를 높이기 어려움
스윕마다 새로운 목적지, 때로는 별도의 볼트가 뒤따름 960359: 13 -> 13 -> 0; 960395: 1,918 -> 294 -> 293 공유 수집기 탐지기 실패; 그룹화는 타이밍, 수수료율, 트랜잭션 템플릿, 오프체인 증거에 의존

추적된 출력이 이동하면, 분석은 수수료 후 가치를 보존하고 귀속을 스윕된 금액으로 제한하면서 분할, 병합, 필체인을 따른다. 거래소나 기타 혼합 서비스로 들어가는 자금은 신뢰도를 낮추며, 해당 서비스는 공격자 클러스터에 추가되지 않는다.

MetaSleuth 조사 탐색

조사를 위한 자금 흐름 추적 및 증거 구축

지금 무료로 사용해보기

수정이 필요했던 수정: 잠재적인 벽돌화 회귀?

엔트로피 오류와는 별개로, 이를 해결하기 위한 핫픽스가 별도의 펌웨어 회귀를 도입했다 [8, 9]. 하드웨어 RNG 경로를 복구하는 과정에서, 수정은 하드웨어 시드 오류 조건을 처리하지 않은 채로 남겨두었는데, 이는 로그인 전에 서비스를 거부할 수 있으며 장치가 영구적으로 벽돌화되었다는 주장까지 제기되었다. 이 강한 주장이 얼마나 타당한지는 레지스터 레벨 세부 사항에 달려 있다.

STM32 하드웨어 RNG는 제어 레지스터(RNG_CR), 상태 레지스터(RNG_SR), 32비트 데이터 레지스터(RNG_DR)를 노출한다. 관련 상태는 다음과 같다:

비트 역할
RNGEN RNG 및 아날로그 노이즈 소스 활성화
DRDY RNG_DR에 데이터 준비 완료를 나타냄; 소프트웨어는 여전히 0을 거부해야 함
SECS / SEIS 현재 시드 상태 테스트 실패 / 래치된 시드 오류 상태
CECS / CEIS 현재 RNG 클럭 결함 / 래치된 클럭 오류 상태

현재 상태와 래치된 상태의 구분이 중요하다. SECS는 현재 노이즈 소스 조건을 나타내고, SEIS는 소프트웨어가 클리어할 때까지 시드 오류 발생을 기록한다. Mk4/Q 패밀리 STM32L4S에서 시드 오류는 새로운 난수 생성을 중단시키며, Mk3 STM32L4에서는 데이터가 여전히 사용 가능할 수 있지만 신뢰해서는 안 된다. 클럭 오류는 별개로, 이 시드 오류 잠금을 유발하지 않는다.

필요한 복구 시퀀스는 STM32 세대에 따라 다르다:

장치 패밀리 문서화된 시드 오류 복구
Mk3 STM32L4 (RM0351, RNG 오류 관리 [10]) SEIS 클리어 후, RNGEN 클리어 및 설정
Mk4/Q 패밀리 STM32L4S (RM0432, RNG 오류 관리 [11]) SEIS 클리어 후, RNG_DR 워드 12개를 읽고 버린 다음, SEIS가 클리어 상태로 유지되는지 확인

7월 31일 엔트로피 핫픽스는 rng_get()이 하드웨어 TRNG로 올바르게 해석되도록 했지만, Mk4/Q 패밀리 rng_get_or_fault()는 시드 오류 복구를 구현하지 않는다. rng_init()RNGEN이 클리어된 경우에만 동작하며, 읽기 루프는 DRDY만 확인한다. 시드 오류가 RNGEN은 활성화된 채로 DRDY를 억제하면, 초기화는 no-op이 되고 각 읽기는 10ms 대기 후 SEIS를 클리어하지 않은 채 OSError(EFAULT)를 발생시킨다.

이는 로그인 전 UI에 도달할 수 있다. 숫자 패드 mempad._start_scan()과 Q keyboard._start_scan() 모두 키 입력 인터럽트에서 스캔 순서를 섞는다. 거기서 RNG 예외가 발생하면 해당 하드웨어 세션의 나머지 동안 PIN 입력과 일반 펌웨어 업그레이드 메뉴를 차단할 수 있다.

코드 레벨의 실패 경로는 신빙성이 있지만, "영구 벽돌화 공격"이라는 강한 주장은 확립되지 않았다. RNG 제어 및 상태 비트는 하드웨어 리셋 시 0으로 초기화되므로, 단일 일시적 오류가 주변 장치를 영구적으로 손상시켜서는 안 된다; 전체 전원 차단 후에도 지속되는지는 아직 입증되지 않았다. 또한 시드 상태 테스트 실패를 원격으로 또는 신뢰할 수 있게 제어적으로 트리거하는 방법도 아직 입증되지 않았다. X 게시물 [8]은 벽돌화가 확인되었다고 주장했지만, 이 게시물이 가리킨 PR인 커뮤니티 제출 PR #692 [9]의 작성자 자신은 레지스터 목 방식으로 결함을 분석했으며, 실제 Mk4/Q 하드웨어에서 재현하지 않았고, 현장 보고를 독립적으로 확인하지 않았다고 밝혔다. 따라서 가장 잘 뒷받침되는 분류는 확인된 영구 벽돌화 공격이 아닌, 잠재적인 로그인 전 서비스 거부 및 신뢰성 회귀이다.

유지 관리자의 자체 수정인 PR #693 [12]은 시드 오류 플래그를 확인하고, 제한된 복구 및 재시도를 추가하고, 의심스러운 샘플을 거부하며, 예상되는 키패드 오류만 포착한다; 이 PR은 2026년 8월 5일 병합되어 커뮤니티 PR #692 [9]를 대체했으며, 해당 PR은 2026년 8월 4일 미병합 상태로 종료되었다.

결론

이번 사건은 지갑 엔트로피 오류로, 영향을 받은 장치 및 워크플로우에서 시드 복구 문제를 암호학적으로 불가능한 문제에서 오프라인 탐색 문제로 전환시켰다. 핵심 엔지니어링 실패는 출하된 펌웨어가 보안에 중요한 시드 생성 API가 실제로 의도한 하드웨어 RNG에 도달했는지 증명하지 않았다는 점이다. 빌드 가드는 매크로 존재 여부와 매크로 값 모두를 확인해야 하며, 암호화 엔트로피에 대한 폴백은 실패 시 닫혀야 하고, CI는 최종 펌웨어 이미지에서 심볼 출처 및 엔드-투-엔드 엔트로피 흐름을 검증해야 한다. 영향을 받은 사용자의 경우, 해결책은 펌웨어 업데이트가 아니다: 업데이트해도 결함 있는 경로에서 이미 생성된 시드는 복구되지 않으며, 이후 패스프레이즈를 추가해도 그 시드의 주소에 이미 보관된 자금은 보호되지 않는다. 해당 자금은 수정된 릴리스에서 새 시드로 구축된 지갑으로 이동해야 한다; 이미 강력하고 고유한 패스프레이즈 뒤에 있던 자금만 시드 단독 열거의 범위 밖에 있었다 [6].

참고 문헌

Sign up for the latest updates
~$88M 손실: COLDCARD 및 LULA 익스플로잇 | BlockSec 위클리
Security Insights

~$88M 손실: COLDCARD 및 LULA 익스플로잇 | BlockSec 위클리

2026년 7월 27일~8월 2일, 비트코인과 BNB 체인에서 약 8,800만 달러 손실을 유발한 두 건의 보안 사고가 발생했습니다. COLDCARD 사고는 하드웨어 지갑 펌웨어의 엔트로피 오류로, RNG 매크로 활성화 여부 미확인으로 결정론적 폴백이 실행되어 약 1,370 BTC(~8,800만 달러)가 탈취됐습니다. BNB 체인의 LULA 토큰은 비즈니스 로직 취약점으로 `recycle()` 함수가 악용되어 PancakeSwap V2 유동성에서 약 57만 8천 달러가 유출됐습니다.

뉴스레터 - 2026년 7월
Security Insights

뉴스레터 - 2026년 7월

2026년 7월 DeFi 3대 사고로 Arbitrum·Solana에서 약 6,790만 달러 손실 발생. AFX Trade는 공급망 공격으로 검증자 서명 권한이 탈취되어 약 2,415만 달러 손실. Ostium OLP 볼트는 오라클 인프라 침해로 약 2,375만 달러 유출. BonkDAO는 공격자가 440만 달러로 의결권 확보 후 악의적 자금 이전을 통과시켜 약 2,000만 달러 손실. 세 사고 모두 프로토콜 보안 범위가 스마트 컨트랙트 코드를 훨씬 초월함을 보여준다.

~$3950만 달러 손실: Allbridge, Wanchain 외 다수 | BlockSec 위클리
Security Audits

~$3950만 달러 손실: Allbridge, Wanchain 외 다수 | BlockSec 위클리

2026년 7월 20~26일 한 주간, 솔라나·이더리움·BNB체인·아비트럼·질리카·카르다노에서 8건의 주요 보안 사고로 약 3,950만 달러의 피해가 발생했다. 주요 사례인 Allbridge Core(~165만 달러)는 동일한 Pool 계정이 두 스왑 역할 모두에 허용된 솔라나 입력 검증 취약점이며, 배포된 바이너리만으로 분석됐다. 기타 사례로는 Wanchain(~50만 달러), Zilliqa(~40만 달러), Lien Finance(~54만 달러)가 있다.

Best Security Auditor for Web3

Validate design, code, and business logic before launch. Aligned with the highest industry security standards.

BlockSec Audit