Categories: 블록체인 글

블록체인 보안의 10가지 얼굴 (4부)


서명 리플레이와 운영·키 관리 실패

시리즈의 마지막 편입니다. 1~3부에서 다룬 취약점은 대부분 온체인에 원인이 있었습니다. 4부는 그 경계를 완전히 넘어갑니다. 하나는 서명이라는 암호학적 증거가 맥락을 벗어나 재사용되는 리플레이 공격이고, 다른 하나는 아예 블록체인 바깥의 키 관리와 운영이 뚫린 경우입니다. 흥미롭게도, 역대 최대 규모 손실의 상당수가 바로 이 “경계 지점”에서 발생했습니다.


9. 서명 재사용 / 리플레이 공격

취약점 개요

서명은 “이 메시지를 이 키의 소유자가 승인했다”는 암호학적 증거입니다. 그런데 서명이 어떤 맥락에서 유효한지를 명시적으로 제한하지 않으면, 같은 서명을 전혀 다른 맥락에서 그대로 재사용할 수 있게 됩니다. 실무에서 흔히 발견되는 누락이 세 가지 있습니다.

  1. nonce 부재 → 같은 서명을 여러 번 재제출할 수 있습니다. 예를 들어 “1 ETH 출금을 승인한다”는 서명 하나로 출금을 반복해서 트리거하는 식입니다.
  2. chainId 부재 (EIP-155 이전) → 한 체인에서 유효한 트랜잭션이 포크된 다른 체인에서도 그대로 유효해집니다. ETH/ETC가 갈라진 초기에 실제로 문제가 되었던 부분입니다.
  3. 도메인 구분자(domain separator) 부재 → 컨트랙트 A를 위해 만든 서명이 컨트랙트 B에서도 그대로 통용됩니다.

익스플로잇 흐름

// 취약: nonce 도 도메인 구분도 없다
function claim(uint amount, bytes memory sig) public {
    bytes32 hash = keccak256(abi.encodePacked(msg.sender, amount));
    address signer = recover(hash, sig);
    require(signer == authorizer, "invalid sig");
    payable(msg.sender).transfer(amount);   // 같은 sig 로 무한 반복 가능
}

한 번 발급받은 유효한 sig를 반복해서 claim에 제출하면, 서명이 소진되었는지를 추적하는 nonce가 없으므로 매번 검증을 통과합니다. 승인은 분명 한 번뿐이었는데, 출금은 서명이 유효한 한 몇 번이고 일어나는 것입니다.

방어 패턴 — EIP-712 구조화 서명

핵심은 서명 대상 데이터에 nonce + chainId + 컨트랙트 주소를 반드시 함께 묶어, 서명이 “이 체인에서, 이 컨트랙트에 대해, 이 한 번의 실행”에만 유효하도록 만드는 것입니다. EIP-712는 바로 이를 표준화한 서명 스킴입니다.

// 안전: EIP-712 도메인 구분자 + nonce + deadline
bytes32 public immutable DOMAIN_SEPARATOR;
mapping(address => uint256) public nonces;

constructor() {
    DOMAIN_SEPARATOR = keccak256(abi.encode(
        keccak256("EIP712Domain(string name,uint256 chainId,address verifyingContract)"),
        keccak256(bytes("ChainLabVault")),
        block.chainid,          // ← 체인 고정
        address(this)           // ← 컨트랙트 고정
    ));
}

function claim(uint amount, uint deadline, bytes memory sig) public {
    require(block.timestamp <= deadline, "expired");     // ← 만료시각
    uint256 nonce = nonces[msg.sender]++;                // ← 1회용 nonce
    bytes32 structHash = keccak256(abi.encode(
        keccak256("Claim(address to,uint256 amount,uint256 nonce,uint256 deadline)"),
        msg.sender, amount, nonce, deadline
    ));
    bytes32 digest = keccak256(abi.encodePacked("\x19\x01", DOMAIN_SEPARATOR, structHash));
    require(recover(digest, sig) == authorizer, "invalid sig");
    payable(msg.sender).transfer(amount);
}

이렇게 하면 같은 서명을 다시 제출해도, nonce가 이미 소진되어 다음번에는 다른 structHash가 계산되므로 검증에 실패합니다. 또한 다른 체인이나 다른 컨트랙트에서는 DOMAIN_SEPARATOR 값 자체가 달라지므로, 그 서명은 애초에 무효가 됩니다. 서명이 특정 맥락에 단단히 묶이는 것입니다.


10. 프라이빗 키 유출 / 브리지 해킹 (Ronin, 2022)

취약점 개요

지금까지의 모든 방어를 완벽하게 구현하더라도, 키를 도둑맞으면 그 모든 것이 한순간에 무너집니다. 컨트랙트 입장에서, 올바른 서명이 담긴 트랜잭션은 정당한 것으로 취급될 수밖에 없으며, 그 키가 사실은 탈취된 것인지를 알아낼 방법이 없습니다. 특히 여러 체인을 잇는 브리지는 막대한 자금을 소수의 검증자 키로 지키는 구조이기 때문에, 공격자들에게 최우선 표적이 됩니다.

익스플로잇 흐름 — Ronin (약 6억 2,500만 달러, 역대 최대급)

Axie Infinity의 Ronin 브리지는 9개 검증자 중 5개의 서명이 모이면 출금을 승인하는 멀티시그 구조였습니다. 표면적으로는 상당히 견고해 보입니다. 그러나 진짜 문제는 온체인이 아니라 오프체인의 키 관리와 사회공학에 있었습니다.

  1. 공격자(북한 라자루스 그룹으로 지목됨)가 가짜 채용 제안을 미끼로 한 정교한 스피어 피싱으로 Sky Mavis 직원들을 노렸습니다.
  2. 이를 통해 검증자 키 4개를 탈취했습니다. 임계값 5에는 아직 하나가 모자란 상태였습니다.
  3. 그런데 과거에 트래픽 부하를 분산하기 위해, Axie DAO가 Sky Mavis에게 서명 권한 1개를 위임해둔 적이 있었고, 이 위임이 나중에 회수되지 않은 채 그대로 남아 있었습니다.
  4. 이 방치된 위임 서명이 5번째 서명 역할을 하며 임계값 5를 채웠고, 공격자는 브리지의 자금을 출금할 수 있었습니다.

여기서 반드시 주목해야 할 점은, 스마트 컨트랙트에는 아무런 버그가 없었다는 사실입니다. 5-of-9 로직은 처음부터 끝까지 명세대로 정확하게 작동했습니다. 무너진 것은 코드가 아니라 키의 물리적 보관과, 한 번 부여한 위임 권한의 수명을 관리하는 운영 체계였습니다.

유사 사례

  • Wormhole (2022, 약 3억 2,500만 달러): 폐기(deprecated)된 함수를 통해 가디언 서명 검증을 우회당해, 12만 wETH가 부정하게 발행되었습니다 (이쪽은 온체인 로직 버그입니다).
  • Harmony Horizon (2022, 약 1억 달러): 2-of-5 멀티시그에서 단 2개의 키만 탈취당해 자금이 유출되었습니다.

브리지가 이렇게 반복적으로 최대 피해처가 되는 이유는 명확합니다. 한곳에 막대한 자금이 집중되는데, 그 방어는 소수의 키에 의존하기 때문입니다. 자금 집중과 방어 집중이 같은 지점에 겹쳐 있는 것입니다.

방어 패턴

  • 충분히 높은 임계값: 5-of-9보다 더 안전한 비율을 설정하고, 각 키를 서로 다른 조직과 물리적 위치에 분산 보관합니다.
  • HSM(하드웨어 보안 모듈): 키가 서버 메모리에 평문으로 노출되는 일이 없도록, 하드웨어 안에 격리합니다.
  • 위임 권한의 수명 관리: 임시 위임에는 반드시 만료 기한을 두고, 회수되었는지를 정기적으로 감사합니다. (Ronin의 결정적 실패 지점이 바로 이것이었습니다.)
  • 정기적 키 회전: 키를 주기적으로 교체해, 설령 키가 탈취되더라도 그 키가 유효한 기간을 제한합니다.
  • 브리지 최소화 아키텍처: 근본적으로는, 신뢰된 검증자에 의존하는 브리지보다 경량 클라이언트나 검증 기반의 트러스트 최소화 설계를 지향합니다.
  • 이상 탐지 + 출금 한도/타임락: 비정상적으로 큰 규모의 출금에 대해 지연이나 상한을 두어, 사고가 발생했을 때 대응할 시간을 확보합니다.

4부 요약

#문제무너진 지점대표 사례핵심 방어
9서명 리플레이서명의 맥락 제한 부재(일반 패턴)EIP-712 · nonce · deadline
10키 유출 / 브리지 해킹오프체인 키·위임 관리Ronin, Wormhole임계값 · HSM · 위임 만료

시리즈 전체 결론

10가지 사례를 관통하는 통찰을 정리하면 이렇습니다.

  1. 취약점은 여러 층위에 걸쳐 있다. 코드(1·2·3·4), 경제 설계(5·6), 합의(7), 권한 경계(8·9), 그리고 오프체인 운영(10)까지 — “코드 감사”만으로는 이 위협들의 절반도 막을 수 없습니다.
  2. 가장 큰 손실은 코드가 아니라 경계에서 났다. Ronin, Wormhole, Harmony 같은 최대 규모 피해는 순수한 컨트랙트 버그보다 브리지, 키 관리, 초기화, 위임 같은 시스템의 이음새에서 발생했습니다. 방어의 초점을 여기에 맞춰야 하는 이유입니다.
  3. 방어는 계층적이어야 한다. Solidity 0.8의 오버플로우 보호처럼 언어가 해결하는 것, ReentrancyGuard처럼 라이브러리가 해결하는 것, TWAP·EIP-712처럼 설계로 해결하는 것, 그리고 HSM·감사·모니터링처럼 운영으로 해결하는 것 — 이 층들이 함께 쌓여야 비로소 견고해집니다.

결국 블록체인 보안은 “완벽한 스마트 컨트랙트 하나”를 짜는 문제가 아닙니다. 코드부터 경제, 합의, 운영에 이르는 모든 층위의 신뢰 경계를 함께 지켜내는 시스템 설계의 문제입니다.

「블록체인 보안의 10가지 얼굴」 시리즈 완결. ChainLab — 세상을 잇는 체인.

Tags: 블록체인보안, EIP712, 서명리플레이, 브리지해킹, 키관리

Recent Posts

블록체인이 진짜 파는 것: “나는 못 고친다”

체인 위의 데이터 — 3부작 중 3편 · ChainLab 앞의 두 편은 배관에 관한 것이었습니다…

2개월 ago

데이터를 소비하는 것이 스마트컨트랙트일 때

체인 위의 데이터 — 3부작 중 2편 · ChainLab 1편에서 우리는 하나의 재정의에 도달했습니다. 블록체인은…

2개월 ago

무료 데이터를 블록체인에 얹어 판다는 것

체인 위의 데이터 — 3부작 중 1편 · ChainLab 솔깃한 아이디어가 하나 있습니다. 부동산 실거래가…

2개월 ago

한국에선 왜 아직? — 규제 이야기, 쉽게 (5/5)

기술은 세계 최고 수준인데, 왜 우리나라에선 AI 결제가 아직 잘 안 굴러갈까요? 답은 '실력'이 아니라…

2개월 ago

에스크로 — 돈을 안전하게 맡기는 금고, 그런데 왜 어려울까 (4/5)

중고거래 안전결제, 다들 써보셨죠? AI 결제에도 이게 필요합니다. 문제는, 생각보다 훨씬 까다롭다는 거예요. 중고거래를 떠올려…

2개월 ago

AP2 & MPP — ‘미리 허락받는’ 결제 vs ‘탭 열어두는’ 결제 (3/5)

앞 편의 x402가 자판기였다면, 이번 두 방식은 '용돈 한도'와 '술집 외상장부'에 가깝습니다. 앞 편 복습:…

2개월 ago