
Ký mù: bạn thực sự đang phê duyệt điều gì khi màn hình chẳng nói gì hữu ích
Có một khoảnh khắc rất cụ thể mà phần lớn những khoản mất mát lớn trong tiền mã hóa xảy ra. Không phải lúc một chiếc khóa bị đánh cắp. Mà là lúc ai đó nhìn vào một lời nhắc ký mà mình không hiểu, tự nhủ chắc là ổn thôi, rồi bấm phê duyệt.
Ngành này có một cái tên cho việc ấy: ký mù. Nó có nghĩa là đặt chữ ký của bạn lên dữ liệu mà bạn không đọc nổi. Và suốt nhiều năm, các ví coi đó là chuyện bình thường.
Một màn hình ký lẽ ra phải làm gì
Lời hứa an toàn của việc tự giữ tài sản đặt trên một giả định: không gì dịch chuyển nếu không có sự phê duyệt của bạn. Đa chữ ký hai trên hai, ví cứng, thiết bị ký cách ly — mỗi thứ đều là một cỗ máy biến ý định của bạn thành một chữ ký.
Cỗ máy ấy đáng giá đúng bằng mức bạn hiểu được điều mình đã phê duyệt.
Nếu màn hình nói «Gửi 0,5 ETH tới 0x8f3C…», sự phê duyệt của bạn có ý nghĩa. Bạn đã đối chiếu nó với điều mình định làm. Nếu màn hình nói «Tương tác hợp đồng — dữ liệu: 0xa22cb465000000000000000000000000d8dA6…0001», sự phê duyệt của bạn chẳng có ý nghĩa gì cả. Bạn không đang đồng ý với một giao dịch; bạn đang đồng ý với một hình chữ nhật đầy số thập lục phân.
Kẻ tấn công biết rõ chúng cần bạn nhìn thấy màn hình nào trong hai màn hình đó.
Điều gì ẩn trong những byte
Phần tải của một giao dịch EVM là calldata: một bộ chọn hàm dài bốn byte, theo sau là các đối số mã hóa theo ABI. Máy đọc được hoàn hảo, còn con người thì mù tịt. Chỉ một nhúm bộ chọn ấy là con đường tiền ra đi.
approve(address,uint256) — bộ chọn 095ea7b3. Cấp cho một hợp đồng quyền tiêu mã thông báo của bạn. Không phải chuyển khoản; đó là một ủy quyền thường trực. Nó chẳng dịch chuyển gì lúc bạn ký, và chính vì thế mà nó lọt qua. Việc rút sạch diễn ra sau, theo lịch của kẻ tấn công.
setApprovalForAll(address,bool) — bộ chọn a22cb465. Bản tương đương cho NFT, và tệ hơn: một chữ ký duy nhất trao cho một bên vận hành quyền trên mọi mã thông báo thuộc bộ sưu tập đó mà bạn nắm giữ, bây giờ và cả về sau. Không có ô số tiền nào để bạn thấy yên tâm.
increaseAllowance(address,uint256) — bộ chọn 39509351. Nạp thêm vào một hạn mức sẵn có. Thường bị bỏ sót vì nó không phải bộ chọn mà ai cũng được dặn phải canh chừng.
transferFrom(address,address,uint256) — bộ chọn 23b872dd. Chuyển mã thông báo từ một địa chỉ đã cấp hạn mức trước đó. Chính là chữ ký cuối cùng tiêu đi cái mà một lệnh approve trước đó đã cho phép.
Cái khuôn cần thấm: giao dịch lấy đi tiền của bạn hiếm khi là giao dịch bạn đã ký. Bạn đã ký một quyền cho phép. Vụ trộm là một giao dịch riêng, gửi đi sau đó, mà bạn không bao giờ nhìn thấy.
Vì sao «không giới hạn» là từ cần tìm
Gần như mọi vụ lạm dụng hạn mức đều có chung một đặc điểm: số tiền trên thực tế là vô hạn.
Các dapp xin phê duyệt không giới hạn vì như thế tiện — duyệt một lần, không bao giờ bị hỏi lại. Kết quả là người dùng bị huấn luyện để cấp quyền tiêu tiền vô hạn như một thói quen, và một giao diện hiển thị 115792089237316195423570985008687907853269984665640564039457584007913129639935 thì chẳng nói với ai điều gì.
Có một chỗ tinh tế khiến những cách làm ngây thơ mắc bẫy. Giá trị chuẩn tắc của «phê duyệt vô hạn» đúng bằng 2²⁵⁶−1, nên phép kiểm tra hiển nhiên là so với hằng số ấy. Nhưng các dapp vẫn thường dùng những con số lớn khủng khiếp khác — một nửa cực đại, 0xff…f0, 10¹⁸ × 10³⁸ — vốn là vô hạn theo mọi nghĩa thực tế mà vẫn trượt phép so bằng chính xác. Một cảnh báo chỉ bật lên đúng ở giá trị mốc là một cảnh báo mà kẻ tấn công né được bằng cách trừ đi một.
Ngưỡng đúng nằm thấp hơn hẳn giá trị mốc và cao hơn hẳn mọi thứ có thật. SSP đánh dấu mọi hạn mức từ 2²⁵⁵ trở lên — chừng 5,8 × 10⁷⁶, vượt mọi nguồn cung ERC-20 có thể có tới hàng chục bậc độ lớn. Không có gì hợp lệ bị đánh dấu nhầm, và việc mài một giá trị xuống ngay dưới cực đại cũng không né được cảnh báo.
Ngưỡng ấy chỉ áp cho những lệnh cấp hạn mức. Với một lệnh chuyển thông thường, «không giới hạn» không phải khái niệm có nghĩa — bạn đang chuyển một số tiền cụ thể — nên ở đó giá trị mốc cực đại được để yên.
SSP làm gì
SSP giải mã calldata thành ngôn ngữ đời thường ngay trên màn hình phê duyệt. Những bộ chọn nhận diện được sẽ hiện lên đúng bản chất: đối tác là ai, số tiền là bao nhiêu, quyền được cấp có vô hạn hay không. Chuỗi thập lục phân thô không còn là nội dung chính — nó nằm sau mục Nâng cao, cho những ai muốn xem.
Ba quyết định thiết kế còn quan trọng hơn cả việc giải mã.
Bộ giải mã chỉ thuộc tầng trình bày. Nó không bao giờ thay đổi thứ được ký. Việc phê duyệt luôn ký đúng phần tải gốc; công cụ này chỉ trình bày lại những byte trước đây được phơi ra dưới dạng thập lục phân thô. Một bộ giải mã có khả năng sửa phần tải sẽ là một bề mặt tấn công mới chứ không phải một lớp phòng vệ — thứ bạn đọc và thứ bạn ký phải luôn là cùng những byte ấy.
Nó thất bại theo hướng đóng. Bộ chọn lạ, độ dài sai, thập lục phân dị dạng, đệm địa chỉ không chuẩn — bất cứ điều gì ngoài dự tính đều trả về rỗng, và giao diện lùi về một hành động chung chung với thập lục phân thô trong mục Nâng cao. Nó không bao giờ đoán. Một bộ giải mã hay đoán còn tệ hơn không có bộ giải mã, bởi một bản tóm tắt sai mà đầy tự tin còn nguy hiểm hơn dãy thập lục phân hiện rành rành: dãy thập lục phân ít nhất còn thành thật nói với bạn rằng bạn không hiểu nó.
Bản năng thất-bại-theo-hướng-đóng ấy đi sâu hơn cả những hàm lạ. Một địa chỉ mã hóa theo ABI là mười hai byte không rồi tới hai mươi byte địa chỉ; một từ có bất cứ thứ gì khác ở những byte đầu ấy là không mã hóa chuẩn tắc, và SSP coi nó là đáng ngờ thay vì cố diễn giải. Với giá trị luận lý cũng vậy — chỉ chấp nhận các cách mã hóa chuẩn tắc: toàn số không (sai) và 31 số không rồi 0x01 (đúng). Cách mã hóa phi chuẩn tắc trên một màn hình cấp quyền tiêu tiền là một lá cờ đỏ, không phải một câu đố phân tích cú pháp.
Ký hiệu và số lẻ của mã thông báo không bao giờ được đoán. Một số tiền dễ đọc chỉ hiện ra khi mã thông báo được biết chắc chắn từ sổ đăng ký nằm ngay trên thiết bị. Nếu không, bạn nhận được con số thô theo đơn vị cơ sở. Điều này cố tình kém đẹp mắt: hiển thị «5,0 USDC» cho một hợp đồng chỉ đơn thuần tự xưng là USDC sẽ biến bộ giải mã thành một cỗ máy nói dối, mà đó chính xác là kết cục kẻ tấn công mong muốn.
Rồi còn có phần thuộc về cấu trúc chứ không phải trang trí. Trong SSP, giao dịch được dựng ở một nơi và phê duyệt ở một nơi khác: soạn trong tiện ích trình duyệt, giải mã và hiển thị độc lập trên điện thoại của bạn, nơi SSP Key tính lại giá trị băm của giao dịch ngay trên máy và từ chối ký nếu nó không khớp với thứ đã được hiển thị. Ký mù trên một thiết bị duy nhất có nghĩa là chỉ cần một màn hình bị xâm phạm là đủ. Ở đây, màn hình cho bạn xem hành động đã giải mã và thiết bị giữ chiếc khóa thứ hai là cùng một thiết bị, và nó xác minh chứ không tin.
Làm gì với những phê duyệt của chính bạn
Hãy coi approve và setApprovalForAll là những cái nguy hiểm. Đó là những chữ ký khiến người ta mất tiền, và chúng có vẻ vô hại chính vì chẳng có gì dịch chuyển.
Từ chối phê duyệt không giới hạn khi bạn có thể. Nhiều dapp chấp nhận một số tiền có giới hạn nếu bạn tự đặt. Đó là ma sát, và nó giới hạn thiệt hại của bạn ở đúng mức bạn thật sự định tiêu.
Rà lại những gì bạn đã cấp. Phê duyệt cũ không hết hạn. Một quyền bạn trao cho một giao thức từ hai năm trước vẫn còn sống, và nếu hợp đồng đó về sau bị xâm phạm thì đó là một lối đi mở tới mã thông báo của bạn. Thu hồi phê duyệt rất nhanh, và đó là giờ dọn dẹp có giá trị cao nhất trong việc tự giữ tài sản.
Khi màn hình chẳng nói gì với bạn, chính điều đó là tín hiệu. Nếu ví của bạn không thể nói một giao dịch làm gì, đó là thông tin — không phải một sự phiền toái để bấm cho qua. Phản ứng đúng trước một lời nhắc không đọc nổi là dừng lại, chứ không phải nheo mắt.
Mục tiêu chưa bao giờ là bắt bạn đọc thập lục phân. Mục tiêu là bảo đảm rằng khi bạn phê duyệt điều gì đó, bạn và chiếc ví của bạn hiểu giống nhau về điều đó là gì.


