Tác giả: enze
Biên tập: Liz
Giới thiệu
Gần đây, thư viện mã hóa elliptic được sử dụng rộng rãi trong hệ sinh thái JavaScript đã được phát hiện có lỗ hổng an ninh nghiêm trọng (GHSA-vjh7-7g9h-fjfh). Kẻ tấn công có thể thông qua việc xây dựng đầu vào cụ thể, trong trường hợp chỉ ký một lần, lấy được khóa riêng, từ đó hoàn toàn kiểm soát tài sản kỹ thuật số hoặc chứng chỉ danh tính của nạn nhân.
Nguyên nhân gốc rễ của lỗ hổng này nằm ở sự thiếu sót trong xử lý đầu vào không chuẩn của thư viện elliptic, dẫn đến việc số ngẫu nhiên k trong chữ ký ECDSA có thể bị trùng lặp. Bởi vì độ an toàn của thuật toán ECDSA cực kỳ phụ thuộc vào tính duy nhất của k, một khi k trùng lặp, khóa riêng có thể được suy ra trực tiếp, gây ra rủi ro an ninh không thể đảo ngược.
Bài viết này sẽ phân tích nguyên lý lỗ hổng, nguyên nhân và tác động, và cung cấp khuyến nghị sửa lỗi, giúp các nhà phát triển giảm thiểu rủi ro an ninh.
Bối cảnh lỗ hổng
Giới thiệu về thư viện elliptic
Elliptic là một thư viện mật mã đường cong ellip được sử dụng rộng rãi trong hệ sinh thái JavaScript, hỗ trợ nhiều đường cong (như secp256k1, ed25519). Thư viện này được nhiều ví tiền điện tử, hệ thống xác thực danh tính, ứng dụng Web3 và các ứng dụng khác sử dụng.
Phạm vi ảnh hưởng
Phiên bản bị ảnh hưởng: elliptic <= 6.6.0
Đường cong bị ảnh hưởng: secp256k1, ed25519, v.v.
Tình huống bị ảnh hưởng: Tất cả các ứng dụng nhận đầu vào bên ngoài để thực hiện chữ ký ECDSA (đặc biệt là những hệ thống nhận đầu vào bên ngoài để ký)
Nếu ứng dụng phụ thuộc vào elliptic để thực hiện chữ ký ECDSA và cho phép người dùng cung cấp thông điệp chưa được lọc làm nội dung ký, rất có thể sẽ bị ảnh hưởng bởi lỗ hổng này.
Nguyên lý lỗ hổng
Số ngẫu nhiên k trong chữ ký ECDSA
Hãy tưởng tượng bạn có một con dấu độc nhất vô nhị (khóa riêng), mỗi lần ký tài liệu, bạn cần lấy một loại mực độc đáo (số ngẫu nhiên k). Dấu ấn sẽ tạo ra hai số r và s, đó chính là kết quả ký.
Quá trình này có vài điểm quan trọng:
Con dấu (khóa riêng) luôn không thay đổi - đại diện cho danh tính của bạn.
Mỗi lần ký đều phải sử dụng một con dấu mới (số ngẫu nhiên k tuyệt đối không được trùng lặp).
Ngay cả khi ký cùng một tài liệu, kết quả ký (r, s) cũng phải khác nhau.
Bất kỳ ai cũng có thể xác minh dấu vết có phải do con dấu của bạn đóng hay không (khóa công khai xác minh chữ ký).
Nhưng không ai có thể từ dấu vết suy ra con dấu của bạn (khóa riêng), miễn là k là an toàn.
Để đảm bảo an toàn cho k, ECDSA áp dụng tiêu chuẩn RFC 6979, sử dụng bộ sinh số ngẫu nhiên xác định (HMAC_DRBG hoặc HKDF) để tính toán k. Khi tạo k, sử dụng khóa riêng d và thông điệp m làm hạt giống, đảm bảo k cố định dưới đầu vào giống nhau. Quy trình tính toán như sau:

Điểm quan trọng:
1. k phải ngẫu nhiên và duy nhất, nếu không sẽ dẫn đến việc rò rỉ khóa riêng.
2. Nếu k giống nhau trong hai lần ký, kẻ tấn công có thể đơn giản phục hồi khóa riêng d trực tiếp từ hai chữ ký (r, s1) và (r, s2).
Suy diễn toán học:
Giả sử người dùng đã ký hai thông điệp khác nhau m1 và m2, nhưng số ngẫu nhiên k lại vô tình giống nhau, thì kết quả ký sẽ như sau:

Kẻ tấn công có thể tính toán sự khác biệt giữa s1 và s2:

Bởi vì m1, m2, s1 và s2 đều là đã biết, kẻ tấn công có thể giải k:

Sau đó, thay k vào phương trình chữ ký gốc, giải ra khóa riêng d:

Phân tích lỗ hổng
Thư viện elliptic đáng lẽ phải sử dụng HMAC_DRBG (bộ sinh số ngẫu nhiên xác định) để đảm bảo mỗi lần ký có k duy nhất. Tuy nhiên, do lỗi xử lý đầu vào, trong một số trường hợp, các thông điệp khác nhau có thể tạo ra cùng một k, dẫn đến rò rỉ khóa riêng.
Mã tạo k
Trong phương thức sign() của thư viện elliptic, k được tạo ra bởi bộ sinh số ngẫu nhiên giả HMAC_DRBG, mã liên quan như sau:
elliptic/lib/elliptic/ec/index.js

Các tham số đầu vào của HMAC_DRBG chủ yếu bao gồm:
entropy (tính ngẫu nhiên) → đây sử dụng khóa riêng bkey, đảm bảo HMAC_DRBG có đủ tính ngẫu nhiên.
nonce (hạt giống ngẫu nhiên) → được tính toán từ msg, ảnh hưởng đến giá trị cuối cùng của k.
Vì HMAC_DRBG là xác định, entropy và nonce giống nhau sẽ tạo ra cùng một k. Nếu nonce giống nhau trong các quá trình ký khác nhau, thì k cũng sẽ giống nhau, điều này sẽ dẫn đến việc rò rỉ khóa riêng.
Nguyên nhân lỗ hổng: Chuyển đổi BN dẫn đến việc tái sử dụng nonce
Trong triển khai của elliptic, msg được chuyển đổi thành BN (Big Number), sau đó nonce được tính toán từ msg.toArray():

Điểm vấn đề:
msg là phiên bản BN, nhưng nonce là mảng.
Các phiên bản BN khác nhau có thể tạo ra cùng một mảng sau khi chuyển đổi.
Điều này có nghĩa là có thể tạo ra cùng một nonce cho các thông điệp khác nhau, cuối cùng dẫn đến việc tái sử dụng k.
Nếu k được tái sử dụng trên hai thông điệp khác nhau msg1 và msg2, thì có thể phục hồi khóa riêng d thông qua công thức toán học đã đề cập ở trên.
Kẻ tấn công có thể xây dựng thông điệp đặc biệt msg sao cho nonce giống nhau, dẫn đến việc tái sử dụng k. Chỉ cần lấy một cặp (r, s1) và (r, s2), có thể phục hồi khóa riêng d thông qua các phép toán toán học. Nghiêm trọng hơn, kẻ tấn công có thể tạo ra các msg độc hại như vậy đối với bất kỳ cặp thông điệp/ký nào đã biết, có nghĩa là chỉ cần dụ nạn nhân ký một thông điệp độc hại, kẻ tấn công có thể hoàn toàn phục hồi khóa riêng và giả mạo bất kỳ chữ ký nào.
Khai thác lỗ hổng (PoC)
Thư viện elliptic cho phép sử dụng chuỗi hex làm một trong các loại đầu vào, trong quá trình ký, msg sẽ được chuyển đổi thành phiên bản BN, rồi chuyển thành loại mảng. Nếu hai thông điệp khác nhau tạo ra cùng một mảng sau khi chuyển đổi, nonce cũng sẽ giống nhau, cuối cùng dẫn đến việc tái sử dụng k.
Ví dụ mã:

Chạy đầu ra:

msg1 = "message" và msg2 = "-message" sau khi chuyển đổi BN, tạo ra cùng một nonce (thiết kế của elliptic chấp nhận chuỗi hex là một trong các loại đầu vào có thể). Do nonce giống nhau, dẫn đến k được tạo ra bởi HMAC_DRBG cũng giống nhau. Từ kết quả ký, có thể thấy giá trị r hoàn toàn giống nhau, đây chính là biểu hiện trực tiếp của việc tái sử dụng k.
Quá trình tấn công hoàn chỉnh:
1. Giả lập nạn nhân ký một thông điệp bình thường, thu được thông điệp msg0 và chữ ký sig0.
2. Kẻ tấn công xây dựng thông điệp độc hại msg1 và dụ nạn nhân ký, thu được sig1.
3. Tận dụng lỗ hổng tái sử dụng k, tính k từ (r, s1) và (r, s2).
4. Tính toán tiếp khóa riêng d từ k, thành công tấn công.
Ví dụ mã:

Chạy đầu ra:

Khuyến nghị sửa lỗi
1. Nâng cấp elliptic lên 6.6.1+, chính thức đã khắc phục vấn đề này.
2. Tránh ký trực tiếp các thông điệp chưa được xác minh, đảm bảo msg đã được chuẩn hóa.
3. Thay đổi khóa riêng có thể bị ảnh hưởng.
Tóm tắt
Lỗ hổng này của elliptic đã phơi bày sự nhạy cảm cực kỳ của ECDSA đối với số ngẫu nhiên k. Một khi k bị tái sử dụng, khóa riêng có thể được phục hồi trực tiếp, kẻ tấn công có thể hoàn toàn kiểm soát tài sản và danh tính của người dùng. Các nhà phát triển nên kịp thời khắc phục lỗ hổng và nghiêm ngặt điều chỉnh xử lý đầu vào để đảm bảo an toàn cho chữ ký ECDSA. Cuối cùng, cảm ơn ví Rabby đã cung cấp thông tin về lỗ hổng.
Tài liệu tham khảo
[1] https://github.com/indutny/elliptic/security/advisories/GHSA-vjh7-7g9h-fjfh
[2] https://paulmillr.com/posts/deterministic-signatures
