Reconix LogoReconix
ภาพประกอบบทความ เมื่อ Hardware Wallet สร้างกุญแจที่เดาได้ ช่องโหว่ RNG ใน COLDCARD Firmware

เมื่อ Hardware Wallet สร้างกุญแจที่เดาได้ ช่องโหว่ RNG ใน COLDCARD Firmware

Reconix Team (Sorawish Laovakul)
BlockchainCrypto

Summary

  • COLDCARD hardware wallet มี bug ที่ทำให้อุปกรณ์ไม่ได้ใช้ hardware TRNG อย่างที่ทีมพัฒนาตั้งใจ แต่ไปใช้ software PRNG ที่คาดเดาได้แทน ทำให้ seed ที่ควรเดาไม่ได้ในทางปฏิบัติ เหลือความเป็นไปได้ในการคาดเดาที่น้อยพอที่ผู้โจมตีจะ brute force จนเจอได้จริง
  • bug นี้อยู่ใน firmware ตั้งแต่ปี 2021 นานกว่า 5 ปีกว่าจะถูกพบ
  • ปลายเดือนกรกฎาคม 2026 เกิด Incident ที่มีความเสียหาย Bitcoin ประมาณ 594 BTC จากราว 500 wallet หายไปในเวลาไม่ถึง 30 นาที และตัวเลขยังคงเพิ่มขึ้นอย่างต่อเนื่อง
  • ใครที่สร้าง wallet ด้วย firmware ช่วงที่มีปัญหา ต้อง update firmware แล้วสร้าง wallet ใหม่และย้ายเงิน เพราะการ update อย่างเดียวไม่ได้แก้ไขเรื่องการใช้ seed เดิมที่สามารถคาดเดาได้

Part 1: Background

COLDCARD คืออะไร

COLDCARD คือ hardware wallet สำหรับ Bitcoin ผลิตโดยบริษัท Coinkite

hardware wallet คืออุปกรณ์ขนาดเล็กที่เก็บ private key ของเจ้าของเหรียญไว้ข้างใน และใช้ private key นั้นเซ็นรับรองธุรกรรม โดย private key ไม่เคยออกจากอุปกรณ์ไปอยู่บนคอมพิวเตอร์หรือมือถือ แนวคิดคือ ต่อให้คอมพิวเตอร์ของเราติด malware ผู้โจมตีก็ยังขโมยเงินไปไม่ได้ เพราะ private key ไม่ได้อยู่บนเครื่องนั้น

COLDCARD เป็นที่นิยมในกลุ่มผู้ใช้ที่ซีเรียสเรื่องความปลอดภัยเป็นพิเศษ เพราะใช้งานแบบ air-gapped ได้ คือไม่ต้องเสียบ USB กับคอมพิวเตอร์เลยแม้แต่ครั้งเดียว ธุรกรรมส่งเข้า-ออกผ่าน MicroSD card เท่านั้น

รุ่นที่ยังใช้งานอยู่ปัจจุบัน ได้แก่ Mk2, Mk3, Mk4, Mk5 และรุ่น Q

"Seed" คืออะไร ทำไมถึงสำคัญ

ตอนตั้งค่า wallet ครั้งแรก อุปกรณ์จะสร้างชุดคำภาษาอังกฤษ 12 หรือ 24 คำ เรียกว่า seed phrase (หรือ recovery phrase) ให้ผู้ใช้จดเก็บไว้

ชุดคำนี้คือตัวกำหนด private key ทั้งหมดของกระเป๋าใบนั้น ไม่ใช่รหัสผ่านสำรอง ใครรู้ seed phrase ก็เป็นเจ้าของเหรียญทั้งหมดทันที ไม่ว่าตัวอุปกรณ์จริงจะอยู่ที่ไหน

seed สร้างขึ้นจากตัวเลขสุ่ม 32 bytes ที่อุปกรณ์สุ่มขึ้นมาเอง ความปลอดภัยทั้งหมดจึงขึ้นอยู่กับว่า "สุ่มดีแค่ไหน"

Entropy วัดกันยังไง

คุณภาพของการสุ่มเรียกว่า entropy วัดเป็น bit โดยทุก 1 bit ที่เพิ่ม หมายถึงจำนวนที่ผู้โจมตีต้องไล่เดาเพิ่มขึ้นเป็น 2 เท่า

  • 128 bits คือระดับที่ตั้งใจไว้ ไล่เดาไม่ไหวแม้จะรวมคอมพิวเตอร์จำนวนมากเข้าด้วยกัน
  • 72 bits คือระดับที่ผู้มีทรัพยากรมากพอสามารถประมวลผลเพื่อคาดเดาได้จริง
  • 40 bits คือระดับที่เครื่องแรง ๆ เครื่องเดียวก็ไล่ได้หมดในเวลาไม่นาน

ทำไม air gap ถึงช่วยไม่ได้

กลไกป้องกันของ hardware wallet ทั้งหมด ไม่ว่าจะเป็น PIN, secure element หรือ air gap ตั้งอยู่บนสมมติฐานเดียวกัน คือ private key อยู่ในอุปกรณ์ และหน้าที่ของอุปกรณ์คือกันไม่ให้ใครเข้าถึงมัน

ช่องโหว่นี้ทำให้สมมติฐานนั้นใช้ไม่ได้ เพราะผู้โจมตีไม่ต้องเข้าถึง private key ในอุปกรณ์เลย แต่สร้าง private key อันเดียวกันขึ้นมาบนคอมพิวเตอร์ของตัวเองได้

เปรียบเทียบให้เห็นภาพแบบง่ายๆเช่น โรงงานทำกุญแจที่โฆษณาว่าฟันกุญแจแต่ละดอกสุ่มไม่ซ้ำกัน แต่จริง ๆ แล้วมีแบบอยู่ไม่กี่แบบ โจรจึงไม่ต้องมางัดบ้านคุณ แค่ทำกุญแจทุกแบบขึ้นมาเองที่บ้านตัวเอง แล้วค่อยเอากุญแจมาไล่ไขดูว่าดอกไหนใช้กับบ้านหลังไหนได้

ในโลกของ blockchain การไล่ดูว่ากุญแจตรงกับ wallet ไหนน้ันสามารถทำได้เพราะข้อมูลบน blockchain เปิดเผยต่อสาธารณะ ผู้โจมตีจึงทำวนขั้นตอนด้านล่างเพื่อหา private key ที่ถูกต้องบนเครื่องของตัวเองได้:

  1. หยิบ seed หนึ่งชุดจากรายการความเป็นไปได้ทั้งหมด
  2. คำนวณ address จาก seed นั้น (เป็น open standard BIP39/BIP32 ใครก็คำนวณได้)
  3. เทียบกับข้อมูลบน blockchain ว่า address นั้นเคยมีเงินเข้าหรือไม่
  4. ถ้าตรง เท่ากับได้ private key ของ wallet ใบนั้นไป ถ้าไม่ตรง ก็กลับไปข้อ 1

ทั้งสี่ขั้นตอนไม่มีขั้นไหนแตะต้องอุปกรณ์ของเหยื่อเลย เหยื่อจึงไม่มีทางรู้ตัวจนกว่าเงินจะหายไปแล้ว

เทียบกับการเดา PIN ต่อให้ PIN มีความเป็นไปได้น้อยกว่า seed มาก แต่ผู้โจมตีต้องเดาผ่านตัวอุปกรณ์ ซึ่งจำกัดจำนวนครั้งและจะ lock หรือล้างข้อมูลเมื่อใส่ผิดเกินกำหนด การเดาจึงถูกเบรกไว้ ขณะที่การเดา seed แล้วเทียบกับ blockchain ไม่มีอะไรมาเบรกเลย เร็วเท่าที่ hardware ของผู้โจมตีจะไหว และ scale เพิ่มเครื่องได้ไม่จำกัด


Part 2: ลำดับเหตุการณ์

วันที่ เหตุการณ์
28 ม.ค. 2021 guard ที่เขียนผิดใน libngu มีอยู่ code แล้ว
1 มี.ค. 2021 COLDCARD ย้าย seed generation ไปเรียกผ่าน libngu (commit b18723dd)
17 มี.ค. 2021 firmware v4.0.0 ที่มีช่องโหว่ถูกปล่อยให้ผู้ใช้
11-14 มี.ค. 2022 Mk4 เพิ่มกลไก secure-element reseed (แต่ใช้เพียง 32 bits)
30 ก.ค. 2026 ผู้ใช้รายงานว่าเงินหาย ประมาณ 594 BTC จากราว 500 wallet ในเวลาไม่ถึง 30 นาที
30 ก.ค. 2026 Coinkite ออก preliminary advisory เผยแพร่ root cause analysis
31 ก.ค. ถึง 1 ส.ค. 2026 Coinkite ปล่อย fixed firmware ครบทุกรุ่น

ช่วงเวลาที่ seed อาจถูกสร้างอย่างไม่ปลอดภัย จึงกินเวลานานกว่า 5 ปี


Part 3: Root Cause Summary

ต้นตอทั้งหมดมาจาก config บรรทัดเดียว ซึ่งตั้งใจจะบอกว่า "ไม่ต้องใช้ RNG ของ MicroPython นะ เพราะเราเขียน wrapper ของเราเองไว้แล้ว" (COLDCARD เรียกผ่าน ckcc.rng_bytes ซึ่งอ่านจาก TRNG จริง)

// We have our own version of this code.
#define MICROPY_HW_ENABLE_RNG (0)

ปัญหาคือ code บรรทัดนี้ถูก code ในส่วนอื่นอ่านความหมายผิดไป 2 ส่วนพร้อมกัน

#define MICROPY_HW_ENABLE_RNG (0)
   ├─→ ส่วนที่ 1  MicroPython: เข้าใจว่าบอร์ดนี้ไม่มี hardware RNG จึงใส่ software PRNG มาแทน
   └─→ ส่วนที่ 2  libngu: guard ที่ใส่มาดักกรณีนี้โดยเฉพาะไม่ทำงาน

ส่วนที่ 1: MicroPython เข้าใจว่าบอร์ดนี้ไม่มี hardware RNG

MicroPython อ่านค่านั้นแล้วสรุปว่าบอร์ดนี้ไม่มี hardware RNG จึงใส่ software PRNG มาแทนให้อัตโนมัติ

#if MICROPY_HW_ENABLE_RNG
    // มี hardware RNG → ใช้ STM32 hardware RNG
#else
    // ไม่มี hardware RNG → ใช้ Yasmarang software fallback
#endif

Yasmarang เป็น PRNG ขนาดเล็กสำหรับงานทั่วไป ไม่ได้ออกแบบมาให้ทนต่อการโจมตี และที่แย่กว่านั้นคือค่าตั้งต้นของมันมาจากสิ่งที่เดาได้:

pad = UID_low32 ^ SysTick->VAL;
n   = RTC->TR;
d   = RTC->SSR;
Value คืออะไร ทำไมถึงไม่ใช่ entropy
MCU UID หมายเลขประจำชิป (ใช้แค่ 32 bits ล่าง) คงที่ตลอดอายุชิป และบางส่วนยังปรากฏใน USB serial number
SysTick counter ที่ reload ทุก 1 ms เป็นไปได้เพียง 80,000-120,000 ค่า (≈ 2^17)
RTC->TR / RTC->SSR เวลาและ subsecond correlated กันเอง และบน Mk2/Mk3 น่าจะคงที่ เพราะ RTC oscillator ถูกปิดตอน startup

ส่วนที่ 2: guard ที่ใส่มาดักกรณีนี้โดยเฉพาะไม่ทำงาน

libngu ซึ่งเป็น cryptographic library ที่ COLDCARD ใช้ ใส่ compile-time guard ไว้ดักกรณีไม่มี hardware TRNG โดยเฉพาะ

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

#ifndef MICROPY_HW_ENABLE_RNG
#error "get a HW TRNG plz"
#endif

แต่ #ifndef เช็กแค่ว่า macro ถูก define ไว้หรือไม่ ไม่ได้เช็กว่า ค่าที่ define คืออะไร ในเมื่อ COLDCARD define ไว้แล้วว่า (0) จึงไม่เข้าเงื่อนไข guard เลยไม่ทำงาน และ build ผ่านโดยไม่มี warning ใด ๆ

เหมือนกฎบอกว่า "ถ้าช่องนี้เว้นว่างให้ตีกลับ" แล้วมีคนกรอกช่องนั้นมาว่า "ไม่มี" ในเมื่อช่องไม่ได้ว่าง กฎจึงไม่ทำงาน

นอกจากนี้ libngu เรียก TRNG ผ่านชื่อ rng_get() แต่ทีมผู้พัฒนา COLDCARD implement ไว้เป็นชื่อ random32() / random_buffer() ตอน build linker จึงจับคู่ rng_get() ไปที่ MicroPython แทน

ทำให้ผลลัพธ์ของการ implement นี้ไม่เป็นไปตามที่ทีมพัฒนาต้องการคือ:

Expectation: สร้าง seed → libngu → hardware TRNG ของชิป STM32
Reality:     สร้าง seed → libngu → software PRNG ของ MicroPython

แล้วทำไมรุ่นใหม่ที่มี secure element ถึงโดนด้วย

ตั้งแต่ Mk4 เป็นต้นมา มีการ reseed จาก secure element ตอน boot ซึ่งควรจะแก้ปัญหานี้ได้

a = callgate.read_rng(1)        # 32 bytes from SE1
b = callgate.read_rng(2)        # 8 bytes from SE2

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

แต่ code hash ข้อมูล 40 bytes แล้วตัดเหลือ 4 bytes ก่อนส่งเข้า reseed() และ reseed() เองก็เขียนทับ state เพียง word เดียว entropy จาก secure element จึงถูกบีบเหลือ 32 bits

expected entropy vs reality entropy

ตารางข้อมูลจาก Coinkite

รุ่น expected entropy reality entropy
Mk4 / Mk5 / Q 128 bits ประมาณ 72 bits
Mk2 / Mk3 128 bits ประมาณ 40 bits หรือต่ำกว่า

ผลจากตารางสอดคล้องกับข้อเท็จจริงที่ว่า wallet ที่ถูกขโมยจริงส่วนใหญ่เป็น wallet เก่าจากยุค Mk3 ที่มี entropy ประมาณ 40 bits หรือต่ำกว่าซึ่งอ่อนที่สุด


Part 4: เวอร์ชันที่ได้รับผลกระทบ และเวอร์ชันที่แก้แล้ว

เวอร์ชันที่ได้รับผลกระทบ

  • Mk2 / Mk3: v4.0.x ถึง v4.1.9 (Coinkite ระบุเริ่มที่ v4.0.1 แต่ Block ระบุว่า v4.0.0 คือ release แรกที่มี code path นี้ ควรถือว่า v4.0.x ทั้งหมดอยู่ในขอบเขต)
  • Mk4 / Mk5: ก่อน standard v5.6.0 หรือ Edge v6.6.0X
  • Q: ก่อน standard v1.5.0Q หรือ Edge v6.6.0QX

เวอร์ชันที่แก้ไขแล้ว

รุ่น เวอร์ชันที่ปลอดภัย
Mk2 / Mk3 4.2.0 ขึ้นไป
Mk4 / Mk5 (standard) 5.6.0 ขึ้นไป
Q (standard) 1.5.0Q ขึ้นไป
Mk4 / Mk5 (Edge) 6.6.0X ขึ้นไป
Q (Edge) 6.6.0QX ขึ้นไป

Part 5: สิ่งที่ผู้ใช้งาน COLDCARD ต้องทำ

การ update firmware แก้เฉพาะการสร้าง seed ใหม่เท่านั้น ไม่ช่วยเรื่อง seed เดิมที่สร้างไปแล้ว และถ้า seed นั้นเคยถูก export ไปใช้ใน wallet อื่น seed นั้นก็ยังเสี่ยงเหมือนเดิม

  1. update firmware เป็นเวอร์ชันที่แก้แล้วก่อน แล้วจึงสร้าง seed ใหม่บนอุปกรณ์
  2. จด backup และ verify wallet fingerprint (XFP) กับ receive address บนหน้าจออุปกรณ์
  3. ส่ง test transaction จำนวนน้อยก่อน ยืนยันว่าเข้าจริง แล้วจึงย้ายยอดที่เหลือ
  4. เก็บ backup เดิมไว้จนกว่าจะยืนยันว่ายอดทั้งหมดมาถึงครบ

บริการที่เกี่ยวข้องจาก Reconix

  • Smart Contract Audit: ตรวจสอบ logic และการใช้งาน cryptography ใน smart contract ก่อน deploy ขึ้น mainnet
  • Secure Code Review: อ่าน source code เพื่อหาช่องโหว่ระดับการออกแบบ เช่น guard ที่ไม่ทำงาน หรือการ link ไปยัง RNG ผิดตัว อย่างที่เกิดในกรณีนี้
  • Penetration Testing: ทดสอบเจาะระบบครบวงจร ทั้ง web, mobile, API และโครงสร้างพื้นฐาน

แหล่งอ้างอิง

บทความ

บทความที่น่าสนใจอื่นๆ

สำรวจบทความอื่นๆ ที่คุณอาจสนใจจากบล็อกของเรา

ภาพประกอบบทความ แกะ Exploit Chain ของช่องโหว่ wp2shell: WordPress RCE ที่ถูกพบโดย GPT5.6

แกะ Exploit Chain ของช่องโหว่ wp2shell: WordPress RCE ที่ถูกพบโดย GPT5.6

25 กรกฎาคม 2026Reconix Team (Natsasit Jirathammanuwat)

วิเคราะห์ wp2shell exploit chain ที่เชื่อม Batch API Route Confusion (CVE-2026-63030) กับ SQL injection (CVE-2026-60137) เข้ากับฟีเจอร์ของ WordPress จนยกระดับจาก pre-auth ไปเป็น RCE เต็มรูปแบบ

ภาพประกอบบทความ รหัสผ่านอย่างเดียวไม่พออีกต่อไป ทำไมคุณต้องเปิด 2FA

รหัสผ่านอย่างเดียวไม่พออีกต่อไป ทำไมคุณต้องเปิด 2FA

17 กรกฎาคม 2026Reconix Team

2FA คือกำแพงชั้นที่สองที่กั้นระหว่างบัญชีของคุณกับคนที่ได้รหัสผ่านคุณไปแล้ว บทความนี้อธิบายว่ามันทำงานอย่างไร แบบไหนปลอดภัยกว่ากัน และควรเปิดกับบัญชีไหนก่อน