
2026년 10월 30일 새벽 5시(한국 시각), 바이낸스의 XRP 지갑 다섯 개가 동시에 잔액을 비웠습니다. 20분 안에 비트코인, 트론, 솔라나 지갑이 뒤따랐고, 이더리움 핫월릿은 "위임 승인" 한 건으로 통째로 넘어갔습니다. 세 시간 동안 11개 체인에서 약 64억 달러, 우리 돈 약 9조 원이 빠져나갔습니다. 암호화폐 역사상 가장 큰 탈취입니다. 그런데 이번에도 블록체인의 암호는 하나도 깨지지 않았습니다. 넘어간 것은 "서명하는 쪽", 곧 바이낸스 자신의 서명 시스템이었습니다.
이 시리즈의 공격을 실제 쇼핑몰 코드에서 직접 재연하고 막아 보는 인프런 강의가 열렸습니다. 실제 해킹 사고로 배우는 Spring Boot·Express 시큐어 코딩 · 이 글과 이어지는 수업: 13-1, 13-2, 13-3, 13-4
이 글의 범위. 공격이 어떻게 이어졌는지를 단계별로 자세히 풀지만, 그대로 따라 할 수 있는 실행 코드(쓸어 가기 컨트랙트, 위임 서명을 만드는 스크립트, 세탁 절차)는 싣지 않습니다. 코드는 모두 방어 쪽 코드이고 값은 가짜입니다. 바이낸스는 아직 최종 원인 보고서를 내지 않았습니다. 사실관계는 바이낸스 발표, FBI 발표, 블록체인 분석 회사 TRM Labs·Elliptic의 분석(2026-11-01 기준)을 따르고, 분석 회사의 판단은 판단이라고 밝힙니다. 수치는 출처마다 조금씩 다릅니다.
| 항목 | 내용 |
|---|---|
| 발생 | 2026-10-29 20:05~23:56 UTC (한국 10-30 05:05~08:56). 주요 유출은 약 20분 |
| 규모 | 약 63억 6천만 달러(TRM, 10개 체인) · 약 64억 3천만 달러(Elliptic, NEAR 포함 11개 체인) · 약 63억 달러(바이낸스) |
| 가장 큰 덩어리 | 이더리움 약 38억 달러(ETH 20만여 개 + 토큰 185종), 비트코인 18,270 BTC(약 14억 7천만 달러) |
| 공격 주체 | FBI가 10-31 북한 TraderTraitor(라자루스 하위 조직)로 확인. TRM·Elliptic도 같은 판단 |
| 최초 침투 | 바이낸스 1차 발표: DevOps 엔지니어 노트북 침해. 이후 "추가 침해 지점" 발견(세부 미공개) |
| 핵심 기법 | 바이낸스 서명 시스템이 공격자가 고른 거래에 서명(TRM 판단). 이더리움에서는 EIP-7702 위임 승인 50건으로 핫월릿을 쓸어 가기 컨트랙트에 넘김 |
| 개인키 유출 | 바이낸스: "개인키 유출 증거는 없다" |
| 동결·회수 | 약 28억 5천만 달러, 44%(Elliptic) · 약 27억 달러(TRM). 대부분 스테이블코인 |
| 고객 피해 | 바이낸스가 SAFU 기금과 회사 자금으로 전액 보전하겠다고 발표. 11-01부터 출금 재개 |

흐름을 세 덩어리로 나누면 이해가 쉽습니다.

체인마다 키 방식과 서명 방식이 다릅니다. 비트코인과 이더리움, XRP와 솔라나는 서로 다른 곡선과 서명 형식을 씁니다. 그런데 30분 안에 모든 체인에서 유효한 서명이 나왔습니다. 이 점이 이번 사건을 이해하는 열쇠입니다.

바이낸스의 첫 발표는 "DevOps 엔지니어의 노트북이 침해됐을 수 있다"였습니다. 그 접근으로 서비스 컨테이너에 닿아 승인되지 않은 출금을 넣었다는 설명입니다(보안업체 Aikido가 정리한 바이낸스 발표). 이후 바이낸스는 조사에서 추가 침해 지점이 나왔다고 밝혔지만 내용은 공개하지 않았습니다. 공동 CEO는 영상에서 "한 사람이 한 일이 아니다", "자기 파괴 장치가 도구와 흔적을 지웠다"고 말했습니다.
노트북이 어떻게 뚫렸는지는 아직 발표되지 않았습니다. 다만 같은 조직(TraderTraitor)의 이전 사건에서 시작점은 늘 사람이었습니다.
이 패턴은 국내편 북한의 코인 탈취: 바이낸스·바이비트·업비트와 코인 해킹 12년 연대기에서 자세히 다뤘습니다. 운영 서버에 닿는 사람의 노트북은 서버만큼 중요한 자산이라는 점이 이번에도 확인됐습니다.
거래소의 핫월릿(바로 출금할 수 있게 온라인에 둔 지갑)은 보통 키를 하드웨어 보안 모듈(HSM)이나 다자간 계산(MPC) 서명 시스템 안에 둡니다. 키는 밖으로 나오지 않고, 출금 서비스가 "이 거래에 서명해 달라"고 요청하면 서명만 돌려줍니다.
TRM은 이렇게 봅니다. 10개 체인에서 서로 다른 키 방식의 유효한 서명이 반 시간 안에 나왔으니, 피싱으로 서명 몇 개를 얻은 것으로는 설명되지 않는다. 가장 그럴듯한 설명은 공격자가 바이낸스 핫월릿 서명 시스템이 자기가 고른 거래에 서명하도록 만들 수 있었다는 것이다. Elliptic도 "공격자가 바이낸스 자신의 시스템이 대신 서명하게 만든 것으로 보인다"고 썼습니다. 바이낸스 역시 개인키 유출 증거는 없다고 했습니다.
이것이 중요한 이유는 이렇습니다. 키를 HSM에 잘 넣어 두는 것만으로는 부족합니다. 서명 시스템이 "요청이 오면 무엇이든 서명하는" 구조라면, 요청하는 쪽을 장악한 공격자에게 키는 이미 넘어간 것과 같습니다. 국내편 쿠팡 사고가 "서명만 맞으면 통과하는 게이트웨이"였다면, 이번에는 "요청만 맞으면 서명하는 서명기"였습니다.

이더리움에서는 다른 체인과 방법이 달랐습니다. 여기서 쓰인 것이 EIP-7702입니다.
EIP-7702는 무엇인가. 2025년 5월 이더리움 펙트라 업그레이드로 들어온 기능입니다. 원래 이더리움의 일반 계정(EOA, 개인키로만 움직이는 계정)은 코드를 가질 수 없었습니다. EIP-7702는 일반 계정이 특정 스마트 컨트랙트의 코드를 "빌려 쓰도록" 위임할 수 있게 합니다. 여러 거래를 한 번에 묶거나 다른 사람이 수수료를 대신 내 주는 기능을 지갑에 붙이려는 것이 원래 목적입니다.
어떻게 동작하나.
0xef0100 + 대상 주소라는 표시가 기록됩니다. 그 뒤로 이 계정을 부르면 대상 컨트랙트의 코드가 이 계정의 권한으로 실행됩니다.공격자에게 매력적인 성질 세 가지.
1. 한 번이면 계속 남습니다. 위임은 다른 위임으로 바꾸거나 해제할 때까지 유지됩니다. 출금마다 서명을 받을 필요가 없습니다.
2. 제출자가 누구든 됩니다. 서명만 얻으면 공격자의 운영 지갑이 제출하면 됩니다.
3. chain_id를 0으로 쓰면 모든 EVM 체인에서 유효합니다. 이번 사건에서 이 성질이 쓰였다는 발표는 없지만, 방어할 때 반드시 막아야 할 성질입니다.
이번 사건에서 위임 승인 50건은 모두 바이낸스 핫월릿 '바이낸스 14'의 키로 유효하게 서명돼 있었고, 공격자의 쓸어 가기(sweeper) 컨트랙트를 가리켰습니다. 이 컨트랙트는 운영 지갑의 지시만 받도록 만들어져 있었습니다(TRM). 바이낸스 공동 CEO는 이 위임이 "평소의 승인 통제를 거치지 않았다"고 말했습니다. 출금 한 건 한 건을 심사하는 통제가 있었더라도, 위임은 출금이 아니라서 그 통제 밖에 있었던 셈입니다.

위임 승인에는 논스가 들어가고, 실행 순간 계정의 논스와 같아야 유효합니다. 문제는 거래소 핫월릿이 쉬지 않고 출금한다는 점입니다. 출금이 나갈 때마다 논스가 하나씩 오르니, 공격자는 위임이 실행될 순간의 논스를 미리 알 수 없습니다.
그래서 공격자는 연속된 논스 50개에 대한 위임 승인을 미리 받아 두고, 50건을 거래 하나에 담아 제출했습니다. 실행 순간 맞는 하나만 성립하고 나머지는 무시됩니다. TRM의 표현으로는 "하나가 맞았고, 공격자에게는 그거면 충분했다"입니다.
여기서 두 가지를 읽을 수 있습니다.

위임이 성립하고 3분 24초 뒤, 쓸어 가기 컨트랙트가 핫월릿에서 ETH 167,464개와 토큰 63종(USDT 16억 1,500만 개 포함)을 한 번에 빼냈습니다. Elliptic에 따르면 이더리움 손실의 3분의 2(약 25억 3천만 달러)가 거래 하나로 나갔습니다.
끝이 아니었습니다. 위임이 계속 남아 있었기 때문에 이 주소로 새로 들어온 입금이 여섯 번 더 쓸려 나갔습니다. 또 이 핫월릿은 바이낸스의 다른 스마트 계정을 실행할 권한을 가지고 있었고, 공격자는 그 권한으로 8억 2,300만 USDC를 더 빼냈습니다. 위임 하나가 그 계정이 가진 모든 권한을 넘긴 것입니다.
교훈은 분명합니다. 위임을 발견했을 때 출금 서비스만 멈추면 안 됩니다. 그 주소로 들어오는 입금 경로를 바로 끊고, 위임을 해제하는 절차가 있어야 합니다. 위임 해제도 같은 키로 새 위임(빈 주소로의 위임)에 서명해야 하므로, 사고 중에 처음 만들 수 있는 절차가 아닙니다. 미리 승인받아 두어야 합니다.
이더리움 밖에서는 특별한 기능이 쓰이지 않았습니다. 서명 시스템이 서명한 평범한 출금 거래였습니다. XRP 지갑 다섯 개는 최소 잔액만 남기고 비워졌고, 트론 지갑은 2분 안에 USDT 6억 1,270만 개와 TRX 4,640만 개를 보냈습니다. 이더리움에서도 다른 핫월릿 하나('바이낸스 16')는 위임 없이 자기 키 서명으로 약 1억 7,400만 달러어치를 보냈습니다(Elliptic).
체인마다 키 방식이 다른데도 모두 뚫렸다는 것은 체인별 서명 인프라와 승인 경로가 충분히 나뉘어 있지 않았다는 뜻으로 읽힙니다. TRM의 권고도 같습니다. "한 행위자가 몇 시간 안에 10개 체인 지갑의 유효한 서명을 얻었다. 서명 인프라와 승인 경로를 체인·지갑 등급별로 나눠라."

훔친 뒤의 움직임은 "동결당하기 전에 바꾸고, 추적당하기 전에 흩는다"로 요약됩니다. 아래는 분석 회사가 관찰한 내용이고, 방법을 안내하려는 것이 아니라 어디서 끊을 수 있는지 보려는 것입니다.
USDT·USDC 같은 스테이블코인은 발행사가 특정 주소의 토큰을 동결할 수 있습니다. 그래서 공격자는 이것부터 처분했습니다. 탈중앙 거래소(DEX)와 시장조성자 견적(RFQ, 큰 금액을 정해진 가격에 바꿔 주는 방식)으로 USDT 4억 3,400만 개를 ETH 160,451개로 바꿨는데, 시세보다 12~26% 손해를 감수했습니다(Elliptic). TRM도 3억 2,500만 USDT를 ETH로 바꾸면서 10% 넘게 손해를 본 거래를 확인했습니다. 손해를 보더라도 동결보다 빨리 움직이는 것이 이 단계의 목표입니다.
다른 지갑은 대출 프로토콜 Aave에 USDT 약 5,990만 개를 담보로 맡기고 ETH 19,762개를 빌렸습니다. 담보로 맡긴 USDT가 나중에 동결되더라도 빌린 ETH는 이미 손에 있습니다. Aave는 해당 시장을 동결해 새 활동을 막았지만, 이미 나간 자금을 되찾지는 못했습니다.
분석 회사들은 바이비트 때처럼 브리지, 스왑 서비스, 장외(OTC) 중개인을 거쳐 거래소에서 현금화할 것으로 봅니다. Chainalysis CEO는 공격자가 이미 수천 개 주소를 썼고, 몇 주 안에 수만 개로 늘 것으로 예상했습니다.

세탁을 막는 쪽에서 할 일.
이번 사건에서 가장 아픈 지점은 서명 시스템이 "요청이 오면 서명"했다는 것입니다. 서버 쪽 방어는 서명하기 전에 묻는 구조를 만드는 데서 시작합니다. 아래 코드는 거래소가 아니어도 결제·포인트·토큰을 다루는 서비스라면 그대로 옮겨 쓸 수 있는 형태로 썼습니다.

가장 중요한 원칙은 임의의 해시나 메시지에 서명하는 API를 두지 않는 것입니다. EIP-7702 위임 승인은 거래가 아니라 따로 서명하는 메시지(앞에 0x05가 붙은 데이터)입니다. 서명 서비스가 "이 해시에 서명해 줘"라는 요청을 받는 구조라면, 위임 승인인지 출금인지 구분할 수 없습니다. 서명 서비스는 완성된 거래를 받아 직접 해석하고, 정책에 맞는 것만 서명해야 합니다.
# 서명 서비스 앞단의 정책 검사 (방어 예시, 값은 모두 가짜)
ALLOWED_TX_TYPES = {0x02} # 일반 EIP-1559 송금만. 유형 4(set code)는 기본 거부
ALLOWED_SELECTORS = {bytes.fromhex("a9059cbb")} # ERC-20 transfer만
ALLOWLIST = {"hot-tier-1": {"0xFAKE0000000000000000000000000000000000A1"}}
def check(req):
if req.kind != "transaction": # 해시·메시지 서명 요청은 받지 않는다
return deny(req, "raw_sign_not_allowed", alert=True)
tx = decode(req.payload) # 서명할 바이트를 서명 쪽에서 직접 해석
if tx.type not in ALLOWED_TX_TYPES:
return deny(req, f"tx_type_{tx.type}", alert=True)
if tx.authorization_list: # 위임 승인이 들어 있으면 무조건 거부
return deny(req, "eip7702_authorization", alert=True)
if tx.to not in ALLOWLIST[req.wallet_tier] and not is_customer_withdrawal(tx, req):
return deny(req, "destination")
if tx.data and tx.data[:4] not in ALLOWED_SELECTORS:
return deny(req, "function")
if over_rate_limit(req.wallet, tx.value):
return hold(req, "rate_limit") # 사람 승인으로 넘김
return require_second_approval(req) # 다른 경로의 승인을 받은 뒤 서명
이번 위임은 "평소 승인 통제를 거치지 않았다"고 했습니다. 승인 통제가 요청 서비스와 같은 망, 같은 자격증명 안에 있으면 함께 넘어갑니다.
# 같은 키에 서로 다른 nonce 서명 요청이 짧은 시간에 몰리면 경보 (방어 예시)
from collections import defaultdict, deque
import time
WINDOW, LIMIT = 60, 3 # 60초 안에 서로 다른 nonce 3개 이상이면 이상
seen = defaultdict(deque) # key_id -> deque[(time, nonce)]
def on_sign_request(key_id, nonce, kind):
if kind == "authorization": # 위임 서명 요청은 한 건만 와도 경보
alert("eip7702_authorization_request", key_id=key_id, nonce=nonce)
q, now = seen[key_id], time.time()
q.append((now, nonce))
while q and now - q[0][0] > WINDOW:
q.popleft()
if len({n for _, n in q}) >= LIMIT:
alert("nonce_range_signing", key_id=key_id, nonces=sorted({n for _, n in q}))
freeze_key(key_id) # 사람 확인 전까지 이 키의 서명 중지
정상 출금은 논스를 하나씩 순서대로 씁니다. 짧은 시간에 아직 쓰지 않은 논스 여러 개에 대한 서명 요청이 온다면, 그것은 "미래에 쓸 서명을 미리 모으는" 행동입니다.
# 지갑별 10분 유출액이 기준을 넘으면 그 지갑의 서명·출금을 자동 중지 (방어 예시)
import redis, time
r = redis.Redis(host="127.0.0.1", port=6379)
LIMIT_USD = {"hot-tier-1": 2_000_000} # 예시 값: 10분에 200만 달러
def record_outflow(wallet, tier, usd):
key = f"outflow:{wallet}"
now = time.time()
r.zadd(key, {f"{now}:{usd}": now})
r.zremrangebyscore(key, 0, now - 600)
total = sum(float(m.decode().split(":")[1]) for m in r.zrange(key, 0, -1))
if total > LIMIT_USD[tier]:
trip_breaker(wallet) # 서명 서비스에 '이 지갑 중지' 전달 + 당직 호출
이번 탈취는 주요 유출이 20분, 전체가 3시간이었습니다. 지갑별 유출 속도에 자동 중지가 걸려 있었다면 피해 범위는 크게 줄었을 것입니다. TRM과 국내편 분석 회사 의견도 같습니다.
일반 계정에는 코드가 없어야 합니다. 위임이 성립하면 계정 코드가 0xef0100으로 시작하는 값으로 바뀝니다. 블록마다 확인하면 위임을 몇 초 안에 알아챌 수 있습니다.
# 감시 대상 핫월릿의 코드를 블록마다 확인 (방어 예시, 노드 주소는 가짜)
import requests
RPC = "https://rpc.example.com"
HOT_WALLETS = ["0xFAKE00000000000000000000000000000000B014"]
def get_code(addr):
body = {"jsonrpc": "2.0", "id": 1, "method": "eth_getCode", "params": [addr, "latest"]}
return requests.post(RPC, json=body, timeout=5).json()["result"]
def check_wallets():
for addr in HOT_WALLETS:
code = get_code(addr)
if code not in ("0x", "0x0"):
delegated_to = "0x" + code[8:] if code.startswith("0xef0100") else None
page("hot_wallet_has_code", wallet=addr, delegated_to=delegated_to)
trip_breaker(addr) # 출금 중지 + 입금 주소 안내 중단
핫월릿에는 당장 출금에 필요한 만큼만 두고, 리필은 한도와 승인 절차를 거치게 합니다. 이번 이더리움 핫월릿 하나에는 ETH 16만 개가 넘게 있었습니다.
이번 사건의 출발점으로 지목된 곳입니다. 서버를 아무리 잠가도 운영에 닿는 사람의 노트북이 열려 있으면 소용없습니다.
TraderTraitor의 단골 수법입니다. 개발자에게 좋은 조건의 채용 제안을 보내고, "과제"라며 저장소를 내려받아 실행하게 합니다.
# 외부에서 받은 과제 저장소는 회사 노트북이 아닌 격리 환경에서, 스크립트 없이 연다 (방어 예시)
docker run --rm -it --network none -v "$PWD/task:/work:ro" node:22-slim bash
# 컨테이너 안에서: 설치 스크립트를 실행하지 않고 의존성만 확인
npm install --ignore-scripts --dry-run
postinstall 같은 설치 스크립트, 실행 파일, 난독화된 코드가 있으면 그 자리에서 멈춥니다.EIP-7702는 일반 사용자 지갑에도 같은 위험을 만듭니다. 피싱 사이트가 "서명 한 번"으로 사용자 지갑 전체를 위임받을 수 있기 때문입니다.
<script src="https://cdn.example.com/wallet-sdk/1.4.2/sdk.min.js"
integrity="sha384-FAKEFAKEFAKEFAKEFAKEFAKEFAKEFAKEFAKEFAKEFAKEFAKE"
crossorigin="anonymous"></script>
<meta http-equiv="Content-Security-Policy" content="script-src 'self' https://cdn.example.com">

이번 사건은 첫 겹(사람과 기기)이 뚫린 뒤 둘째 겹(서명 정책)과 셋째 겹(출금 한도·감시)에서 멈추지 못했습니다. 넷째 겹(발행사 동결·차단 목록)이 사후에 44%를 건졌습니다. 어느 한 겹도 완벽하지 않다는 전제로, 한 겹이 뚫려도 다음 겹에서 멈추도록 설계하는 것이 이 글의 결론입니다.
오늘 본 사고의 원인은 대부분 서버와 코드의 기본기에서 막을 수 있는 것들이었습니다. 직접 손으로 막아 보고 싶다면 이런 강의가 있습니다.
실제 해킹 사고로 배우는 Spring Boot·Express 시큐어 코딩
서버 보안 기초부터 차근차근 시작하고 싶다면 이 강의도 있습니다.
백엔드 개발자를 위한 서버 보안 실무: 해킹 방어부터 정보유출 방지까지
수치와 사실은 2026년 11월 1일 기준 공개 발표·분석입니다. 바이낸스는 최종 원인 보고서를 내지 않았고, 수사·분석이 진행 중이라 바뀔 수 있습니다. 원화는 1달러 1,400원 기준으로 환산했습니다. 이 글은 법률 자문·투자 조언이 아닙니다.
다음 글 두 번 해석되는 요청: 파서 차이, 요청 밀반입, SMTP 밀반입을 서버에서 막는 법
시리즈 전체 목록 해킹 방식 VS 방어 방법 전체 목록