바이비트 해킹 사건, 그리고 공격이 지나간 경로

2025년 2월 21일, 바이비트는 Safe 멀티시그 콜드 월렛에서 약 15억 달러를 잃었습니다. 업계 역사상 최대 규모의 도난이고, 꼼꼼히 읽어 볼 가치가 있습니다. 단 한 가지를 빼면 거의 모든 것이 제대로 되어 있었기 때문입니다.

무슨 일이 있었나

공개된 사후 분석을 요약하면 다음과 같습니다.

  1. 공격자가 Safe{Wallet} 개발자의 컴퓨터를 장악하고, Safe{Wallet} 프런트엔드를 서비스하는 AWS S3 버킷에 악성 JavaScript를 심었습니다. 코드는 2월 19일에 심어졌고 2월 21일에 작동했으며, 바이비트의 특정 Safe를 노렸습니다.
  2. 바이비트의 서명자들은 인터페이스를 열고, 평범해 보이는 거래를 검토했습니다.
  3. 실제로 하드웨어 지갑에 전달된 페이로드는 그 거래가 아니었습니다. Safe 프록시의 masterCopy(슬롯 0)를 덮어써 계정 구현 전체를 공격자의 것으로 바꾸는 delegatecall이었습니다.
  4. 서명자들은 승인했습니다. 서명은 유효했습니다. 컨트랙트는 지시받은 그대로 실행했습니다.

공격 주체로는 북한 연계 세력이 공개적으로 지목되었습니다(FBI는 TraderTraitor 그룹을 지목했습니다).

뚫리지 않은 것

  • Safe 컨트랙트가 아닙니다. 컨트랙트는 유효하게 서명된 명령을 실행했을 뿐입니다. Safe의 버그를 악용한 것이 아닙니다.
  • 암호 기술이 아닙니다. 서명은 모두 진짜였습니다.
  • 하드웨어 지갑이 아닙니다. Ledger 기기가 과정에 있었고, 그래도 서명했습니다. 하드웨어 지갑은 전달받은 것을 보여 줄 뿐인데, 전달받은 것이 바로 악성 페이로드였기 때문입니다. delegatecall을 사람이 판단할 수 있는 형태로 풀어 주지 못하는 기기는 키를 지킬 뿐, 판단을 지켜 주지는 못합니다.

뚫린 것은 모든 지갑 인터페이스의 밑바탕에 있는 가정입니다. 거래를 설명하는 화면과 실제로 서명되는 바이트가 같다는 가정입니다.

왜 이것이 특이한 사건이 아니라 일반적인 경우인가

웹 지갑에서 만들어지는 서명은 대부분 그 가정 위에 있습니다. 인터페이스가 페이로드를 만들고, 인터페이스가 요약을 그려 보여 주며, 둘이 일치하는지 독립적으로 확인하는 것은 없습니다. 그 인터페이스를 서비스하는 코드가 바뀌면(침해된 빌드 파이프라인, 탈취된 CDN, 악성 의존성, 도난당한 배포 자격 증명 등) 요약은 공격자가 원하는 무엇이든 될 수 있고, 서명은 진짜입니다.

Vela의 서명 설계가 겨냥하는 위험이 바로 이것입니다. 피싱도, 유출된 키도 아닙니다. 거짓말을 하는 서명 화면입니다.

Vela는 무엇을 하나

calldata 수준까지 가는 클리어 서명. 모든 거래는 승인하기 전에 사람이 읽을 수 있는 의도로 디코딩됩니다. 금액, 받는 사람, 호출이 실제로 하는 일입니다 (ERC-7730). 디코딩하지 못한 호출은 괜찮은 것처럼 슬쩍 보여 주지 않고 디코딩 불가로 표시합니다. 바이비트의 페이로드는 구현 주소를 바꿔치기하는 delegatecall이었습니다. 바로 그런 형태의 호출 앞에서 서명자는 멈춰 서야 했고, 친절한 요약 뒤에 그것이 숨어 있었기 때문에 멈추지 못했습니다.

정확히 짚어 둘 것이 두 가지 있습니다. dApp은 Vela에 delegatecall을 직접 요청할 수 없습니다. 웹페이지가 보낼 수 있는 요청은 일반 호출만 만들어 내므로, 바이비트 페이로드 자체는 이 경로로 들어올 수 없습니다. 그리고 Safe가 자기 자신을 호출하도록 요청하는 웹페이지는 디코딩만 되는 것이 아니라 거부됩니다. enableModule, addOwnerWithThreshold, swapOwner, setFallbackHandler, setGuard를 비롯한 같은 계열의 호출은 대상이 내 지갑이면 차단되며, 묶음 거래 안에서도 MultiSend 안에서도 마찬가지입니다. 대상이 무엇이든 delegatecall이 들어 있는 항목도, SafeTx 타입 데이터 서명도 함께 차단됩니다. 지갑은 어떤 호출을 거부했는지 알려 주고, 서명할 것은 아무것도 내놓지 않습니다. 이 중 어느 것이든 한 번만 서명하면 바이비트 페이로드만큼이나 완전히 계정을 넘겨주게 되고, 활성화된 모듈은 이후 스스로 delegatecall을 실행할 수 있습니다. 그리고 Safe{Wallet}처럼 Vela 자체의 코드가 바뀐다면 디코딩 결과도 공격자의 것이 됩니다. 다음 항목은 바로 그 경우를 위한 것입니다.

인터페이스를 확인할 수 있는 독립된 경로. Vela의 Trusted Signer는 sign.getvela.app에 있는 단일 파일 페이지로, 요청을 스스로 디코딩하고 WebAuthn 서명도 스스로 수행합니다. 처음부터 끝까지 읽을 수 있고, 바이트 하나까지 똑같이 다시 빌드할 수 있고, 직접 서비스할 수도 있는 페이지입니다. 메인 앱과 공급망을 공유하지 않는 두 번째 의견입니다. 앱은 요청을 넘겨주기만 하고, 무엇에 서명할지는 페이지가 직접 계산합니다. 상태: 공개되었습니다. 데스크톱, iPhone, Android 앱은 지갑을 만들거나 로그인할 때 이것을 선택한 기기에서 이것을 사용합니다. 데스크톱 앱은 페이지를 앱에 내장된 버전들과 비교하지만 불일치를 로그에 남길 뿐이고, 휴대폰 앱은 아직 이 확인을 하지 않습니다.

우리에게는 빼앗길 관리자 역할이 없습니다. Vela 계정은 수정하지 않은 Safe v1.4.1이고, Vela는 거기에 어떤 특권 역할도 갖고 있지 않습니다. 관리자 키도 없고, 강요받거나 침해당해 쓰게 될 우리 쪽 업그레이드 경로도 없습니다. 하지만 이것이 없애 주지 않는 것도 분명히 해 두겠습니다. 바이비트 공격자들이 쓴 기본 수단, 즉 소유자가 서명해서 계정 구현을 다시 쓰는 delegatecall은 Vela를 포함한 모든 Safe에 여전히 존재합니다(Vela의 묶음 거래도 delegatecall로 Safe의 MultiSend를 호출합니다). 이를 실행하려면 키 중 하나의 유효한 서명이 필요합니다. 속아서 그 서명을 내주지 않도록 막는 것이 위에서 말한 디코딩과 독립된 확인 경로입니다.

서명마다 새로 확인합니다. 모든 서명에는 키 자체의 확인이 필요합니다. Face ID, 지문, PIN, 또는 보안 키 터치와 PIN입니다. 오래 유지되는 세션 키가 없으므로, 사용자가 없는 사이에 무언가가 대신 서명할 수 있는 틈도 없습니다.

최후의 보루는 셀프 호스팅. 앱과 백엔드 서비스는 오픈소스입니다. 우리의 빌드 파이프라인을 전혀 믿고 싶지 않다면 확장 프로그램이나 앱을 직접 빌드하고 필요한 서비스를 직접 운영하세요. 셀프 호스팅 가이드에서 방법을 안내합니다. 그러면 우리의 빌드 파이프라인은 신뢰 사슬에서 빠집니다. 직접 빌드한 코드는 여전히 믿어야 하니, 코드를 읽어 보세요.

Vela가 주장하지 않는 것

Vela의 프런트엔드도 Safe{Wallet}과 같은 방식으로 침해될 수 있습니다. 우리 코드는 감사를 받지 않았습니다. 그렇지 않은 것처럼 말하는 것이야말로 이 사건으로 끝났어야 할 종류의 보증입니다.

이 설계가 하려는 일은 경로를 좁히는 것입니다. 페이로드를 알아볼 수 없는 덩어리가 아니라 읽을 수 있게 만들고, 사용자를 상대로 악용될 수 있는 관리자 역할을 갖지 않고, 우리가 아닌 무언가로 검증할 방법을 드리는 것입니다. 솔직하게 요약하면, 이런 유형의 공격은 설계로 완화했을 뿐 없앤 것은 아닙니다. 방어를 더 단단하게 만들 부분은 아직 끝나지 않은 채로 감사와 알려진 문제에 적혀 있습니다.

출처