
블라인드 서명: 화면이 쓸모 있는 말을 하나도 하지 않을 때 당신은 실제로 무엇을 승인하는가
큰 암호화폐 손실은 대부분 아주 특정한 순간에 일어납니다. 키를 도둑맞는 순간이 아닙니다. 누군가 이해하지 못하는 서명 요청을 들여다보다가, 아마 괜찮겠지 하고 판단하고, 승인을 누르는 순간입니다.
업계에는 이것을 가리키는 이름이 있습니다. 블라인드 서명. 읽을 수 없는 데이터 위에 자기 서명을 얹는 일입니다. 그리고 오랜 세월 지갑들은 그것을 당연한 일로 취급해 왔습니다.
서명 화면이 해야 할 일
자기 보관의 안전 약속은 하나의 전제 위에 서 있습니다. 당신의 승인 없이는 아무것도 움직이지 않는다는 전제입니다. 2-of-2 멀티시그, 하드웨어 지갑, 망 분리 서명기 — 그 하나하나가 당신의 의도를 서명으로 바꾸는 기계입니다.
그 기계의 값어치는 당신이 승인한 것을 얼마나 이해했는가와 정확히 같습니다.
화면이 "0x8f3C…로 0.5 ETH 보내기" 라고 말한다면, 당신의 승인은 뭔가를 의미합니다. 하려던 일과 견주어 보았으니까요. 화면이 "컨트랙트 상호작용 — 데이터: 0xa22cb465000000000000000000000000d8dA6…0001" 이라고 말한다면, 당신의 승인은 아무 의미도 없습니다. 당신은 거래에 동의하는 것이 아니라, 16진수로 된 직사각형에 동의하고 있는 것입니다.
공격자는 이 두 화면 중 어느 쪽을 당신에게 보여줘야 하는지 압니다.
바이트 속에 숨은 것
EVM 거래의 페이로드는 calldata입니다. 4바이트짜리 함수 선택자 뒤에 ABI로 인코딩된 인자들이 따라붙습니다. 기계에게는 완벽하게 읽히고, 사람에게는 완벽하게 불투명합니다. 돈이 빠져나가는 통로는 그 선택자들 가운데 한 줌입니다.
approve(address,uint256) — 선택자 095ea7b3. 어떤 컨트랙트에 당신의 토큰을 쓸 권한을 줍니다. 전송이 아니라 상시 위임입니다. 서명하는 순간에는 아무것도 움직이지 않고, 바로 그래서 슬쩍 통과합니다. 비워내는 일은 나중에, 공격자의 일정에 맞춰 일어납니다.
setApprovalForAll(address,bool) — 선택자 a22cb465. NFT판이며, 더 나쁩니다. 서명 하나가 그 컬렉션에서 당신이 가진 모든 토큰에 대한 권한을 운영자에게 줍니다. 지금 가진 것도, 앞으로 가질 것도요. 안심할 만한 금액 칸조차 없습니다.
increaseAllowance(address,uint256) — 선택자 39509351. 이미 있는 허용치를 더 채웁니다. 다들 지켜보라고 들은 그 선택자가 아니어서 자주 놓칩니다.
transferFrom(address,address,uint256) — 선택자 23b872dd. 이미 허용치를 내준 주소로부터 토큰을 옮깁니다. 이전의 approve가 허락한 것을 마침내 써버리는 서명입니다.
몸에 익힐 만한 패턴은 이것입니다. 당신의 돈을 훔쳐 가는 거래는 당신이 서명한 그 거래인 경우가 드뭅니다. 당신이 서명한 것은 권한입니다. 절도는 나중에 보내지는 별개의 거래이고, 당신은 그것을 결코 보지 못합니다.
왜 "무제한"이 찾아야 할 단어인가
허용치를 노린 악용은 거의 모두 한 가지 특징을 공유합니다. 금액이 사실상 무한하다는 것입니다.
디앱들이 무제한 승인을 요구하는 이유는 편해서입니다 — 한 번 승인하면 다시는 묻지 않으니까요. 그 결과 사용자들은 무제한 지출 권한을 습관처럼 내주도록 길들여지고, 115792089237316195423570985008687907853269984665640564039457584007913129639935를 보여주는 인터페이스는 아무에게도 아무것도 알려주지 않습니다.
순진한 구현이 걸려 넘어지는 미묘한 지점이 있습니다. "무한 승인"의 표준 값은 정확히 2²⁵⁶−1이므로, 가장 뻔한 검사는 그 상수와 비교하는 것입니다. 그런데 디앱들은 다른 천문학적 숫자들도 일상적으로 씁니다 — 최댓값의 절반, 0xff…f0, 10¹⁸ × 10³⁸ — 이것들은 실질적으로 모든 의미에서 무제한이면서도 정확한 일치 검사는 빠져나갑니다. 정확한 감시 값에서만 울리는 경고는, 공격자가 1을 빼는 것만으로 돌아갈 수 있는 경고입니다.
올바른 문턱값은 감시 값보다 한참 아래이면서, 현실의 어떤 값보다도 한참 위에 있습니다. SSP는 2²⁵⁵ 이상인 허용치를 모두 표시합니다 — 대략 5.8 × 10⁷⁶으로, 가능한 어떤 ERC-20 공급량도 수십 자릿수만큼 넘어섭니다. 정당한 것이 잘못 표시되는 일은 결코 없고, 최댓값 바로 아래로 값을 깎아도 경고를 피하지 못합니다.
그 문턱값은 허용치를 부여하는 호출에만 적용됩니다. 평범한 전송에서 "무제한"은 뜻이 통하는 개념이 아니므로 — 당신은 특정 금액을 옮기는 것이니까요 — 거기서는 정확한 최댓값 감시 값을 건드리지 않습니다.
SSP가 하는 일
SSP는 승인 화면에서 calldata를 평이한 말로 풀어냅니다. 인식된 선택자는 실제 그것이 무엇인지로 그려집니다. 상대가 누구인지, 금액이 얼마인지, 부여되는 권한이 무제한인지. 날것의 16진수는 더 이상 주된 내용이 아닙니다 — 원하는 사람을 위해 고급 항목 뒤에 살고 있습니다.
세 가지 설계 결정이 해독 그 자체보다 중요합니다.
해독기는 오직 표현 계층입니다. 서명되는 것을 결코 바꾸지 않습니다. 승인은 언제나 원래 페이로드 그대로에 서명하고, 이 보조 도구는 예전에 날것의 16진수로 보여주던 바이트를 다시 보여줄 뿐입니다. 페이로드를 바꿀 수 있는 해독기는 방어가 아니라 새로운 공격 표면이 됩니다 — 당신이 읽는 것과 서명하는 것은 언제나 같은 바이트여야 합니다.
닫히는 방향으로 실패합니다. 알 수 없는 선택자, 잘못된 길이, 망가진 16진수, 표준을 벗어난 주소 채움 — 예상 밖의 것은 무엇이든 아무것도 돌려주지 않고, 화면은 고급 항목에 날것의 16진수를 둔 채 일반적인 동작으로 물러납니다. 결코 넘겨짚지 않습니다. 넘겨짚는 해독기는 해독기가 없는 것보다 나쁩니다. 자신만만하게 틀린 요약이 눈에 보이는 16진수보다 위험하기 때문입니다. 16진수는 적어도 당신이 그것을 이해하지 못한다고 정직하게 말해 줍니다.
닫히는 방향으로 실패한다는 이 본능은 알 수 없는 함수보다 더 깊이 뻗어 있습니다. ABI로 인코딩된 주소는 12개의 0바이트 뒤에 20바이트의 주소가 오는 형태입니다. 그 앞쪽 바이트에 다른 무엇이 들어 있는 워드는 표준 인코딩이 아니고, SSP는 그것을 해석하려 들기보다 의심스러운 것으로 다룹니다. 불리언도 마찬가지입니다 — 표준 인코딩만 받아들입니다. 모두 0(거짓), 그리고 0이 31개 뒤에 0x01(참). 지출 권한을 주는 화면에서 비표준 인코딩은 파싱 문제가 아니라 붉은 깃발입니다.
토큰 기호와 소수 자릿수는 결코 넘겨짚지 않습니다. 사람이 읽을 금액은 그 토큰이 기기 안의 등록부에서 확실히 알려져 있을 때만 표시됩니다. 그렇지 않으면 기본 단위의 날것 숫자가 나옵니다. 이는 의도적으로 덜 예쁩니다. 스스로를 USDC라 부를 뿐인 컨트랙트에 대해 "5.0 USDC"를 보여준다면 해독기는 거짓말하는 기계가 될 테고, 그것이야말로 공격자가 바라는 결말입니다.
그리고 겉치레가 아니라 구조에 속하는 부분이 있습니다. SSP에서 거래는 한 곳에서 만들어지고 다른 곳에서 승인됩니다. 브라우저 확장에서 조립되고, 당신의 휴대폰에서 독립적으로 해독되어 표시되며, 거기서 SSP Key가 거래 해시를 기기에서 다시 계산해 보여준 것과 맞지 않으면 서명을 거부합니다. 한 기기에서의 블라인드 서명은 화면 하나만 장악되면 충분하다는 뜻입니다. 여기서는 해독된 동작을 보여주는 화면과 두 번째 키를 쥔 기기가 같은 기기이고, 그 기기는 믿는 대신 검증합니다.
당신 자신의 승인을 어떻게 다룰까
approve와 setApprovalForAll을 위험한 쪽으로 다루십시오. 사람들에게서 돈을 앗아가는 것이 바로 이 서명들이고, 아무것도 움직이지 않기에 무해해 보일 뿐입니다.
가능할 때는 무제한 승인을 거절하십시오. 많은 디앱이 당신이 상한을 정해주면 그 금액을 받아들입니다. 번거로움이지만, 손실을 실제로 쓰려던 만큼으로 묶어 둡니다.
이미 내준 것을 점검하십시오. 오래된 승인은 만료되지 않습니다. 2년 전 어떤 프로토콜에 준 권한은 아직 살아 있고, 그 컨트랙트가 나중에 뚫리면 그것은 당신의 토큰으로 통하는 살아 있는 길이 됩니다. 승인 취소는 금방 끝나고, 자기 보관에서 가장 값진 한 시간의 정리 작업입니다.
화면이 아무 말도 하지 않을 때, 그것이 바로 신호입니다. 지갑이 어떤 거래가 무엇을 하는지 말하지 못한다면, 그것은 정보이지 클릭으로 뚫고 지나갈 불편이 아닙니다. 읽을 수 없는 요청에 대한 옳은 반응은 눈을 가늘게 뜨는 것이 아니라 멈추는 것입니다.
목표는 결코 당신에게 16진수를 읽히는 것이 아니었습니다. 무언가를 승인할 때 그것이 무엇이었는지에 대해 당신과 당신의 지갑이 같은 이해에 있도록 만드는 것입니다.


