Back to Blog

~$3950만 달러 손실: Allbridge, Wanchain 외 다수 | BlockSec 위클리

Code Auditing
July 30, 2026
15 min read
Key Insights

지난 한 주(2026/07/20 - 2026/07/26) 동안 총 약 $39.5M의 손실을 수반한 주목할 만한 보안 사고 8건이 소개됩니다.

날짜 사고 유형 예상 손실
2026/07/19* Allbridge 결함 있는 입력 유효성 검사 ~$1.65M
2026/07/20 Zilliqa 결함 있는 논스 생성 ~$400K
2026/07/20 Wanchain 결함 있는 메시지 인코딩 ~$500K
2026/07/22 42DAO 개인 키 탈취 ~$900K
2026/07/22 AFX Trade 개인 키 탈취 ~$24.15M
2026/07/22 B² Network 개인 키 탈취 ~$3.8M
2026/07/23 Verus 개인 키 탈취 ~$7.6M
2026/07/24 Lien Finance 결함 있는 유효성 검사 로직 ~$542K

*Allbridge 사고는 7월 19일(17:51 UTC)에 발생하였으며 지난주 보고서에는 포함되지 않았습니다. 완전한 기록을 위해 이번 보고서에 포함합니다.

  • Zilliqa는 오프체인 지갑 구현에서 오랫동안 감지되지 않은 취약점을 노출시켰기 때문에 선정되었습니다. 이 취약점은 2019년부터 레거시 네이티브 ZIL 계정 사용자들을 개인 키 탈취 위험에 노출시켜 왔습니다.
  • Allbridge는 Solana의 위치 기반 계정 모델에 의해 가능해진 계정 별칭(account-aliasing) 취약점을 보여주기 때문에 선정되었습니다. 소스 코드를 이용할 수 없어 배포된 프로그램 바이너리로부터 취약점을 재구성해야 했습니다.
  • Wanchain은 암호학을 손상시키지 않고도 브리지를 탈취할 수 있음을 보여주기 때문에 선정되었습니다. 서명은 유효하고 올바르게 검증되었지만, 비단사적(non-injective)이고 구분자가 없는 메시지 인코딩으로 인해 3,097.56 토큰에 대한 승인이 Cardano에서 203,001,692.164714 토큰으로 재해석되었습니다.
  • Lien Finance는 매칭된 항목의 신원과 중복 여부를 확인하지 않고 잔액이 수량과 일치하는지만 검증하는 방식이 어떻게 우회될 수 있는지를 잘 보여주기 때문에 선정되었습니다.

Web3 최고의 보안 감사 기관

출시 전 설계, 코드, 비즈니스 로직을 검증하세요

주간 하이라이트: Allbridge Core

이 사고는 계정 별칭 패턴이 동일한 가변 계정을 두 개의 명령 역할로 받아들이는 모든 Solana 프로그램에 적용될 수 있기 때문에 강조됩니다. 기본 메커니즘인 두 번째 쓰기가 첫 번째 쓰기를 조용히 덮어쓰는 동일한 상태에 대한 두 개의 가변 뷰는 고전적인 ERC-20 자기 전송 버그(from == totransferFrom)와 동일한 구조입니다. 프로젝트의 사후 분석 보고서 [1]는 개요를 제공하지만 코드 수준의 메커니즘을 자세히 설명하지 않습니다. 아래 분석은 그 공백을 메우기 위해 배포된 프로그램 바이너리로부터 재구성되었습니다.

2026년 7월 19일(17:51 UTC), Solana의 크로스체인 브리지 프로토콜인 Allbridge Core가 약 $1.65M(~$1.12M의 USDC와 ~$539K의 USDT)을 탈취당했습니다. 근본 원인은 스왑 명령이 두 가지가 서로 다른지를 강제하지 않은 채 동일한 Pool 계정을 송신 및 수신 역할 모두에서 받아들였다는 것입니다. 동일한 계정이 두 번 전달되었을 때, 한 역할의 내부 회계 업데이트가 다른 역할에 의해 조용히 덮어씌워졌고, 그 사이 실제 토큰 전송은 이미 완료된 상태였습니다. 다섯 번의 셀프 스왑으로 Pool의 기록된 상태가 충분히 왜곡되어 공격자는 소량의 USDT 입력을 ~$2.24M USDC로 전환할 수 있었습니다.

배경

Allbridge Core는 vUSD라는 내부 회계 단위를 통해 스왑을 처리하는 크로스체인 브리지입니다. 스왑은 두 개의 회계 구간으로 구성됩니다:

source token -- swap_to_v_usd(send_pool) --> vUSD
vUSD        -- swap_from_v_usd(receive_pool) --> destination token

vUSD는 SPL 토큰이 아닙니다. 각 Pool의 온체인 데이터 내 필드로만 존재합니다. 각 Pool은 token_balance, v_usd_balance, reserves를 추적합니다. 송신 구간은 소스 토큰을 사용자로부터 송신 브리지 볼트로 전송하고, 송신 Pool의 토큰 측 회계를 증가시키며, vUSD 출력을 계산합니다. 수신 구간은 해당 vUSD를 수신 Pool에 추가하고, 토큰 측 회계를 감소시키며, 수신 브리지 볼트에서 사용자에게 목적지 토큰을 전송합니다.

Solana 프로그램은 호출자로부터 위치 배열 형태로 계정을 받습니다. 프로그램의 명령 핸들러는 어떤 배열 위치가 어떤 역할(send_pool, receive_pool, send_mint 등)에 해당하는지를 지정하지만, 런타임은 호출자가 두 위치에 동일한 계정 주소를 전달하는 것을 막지 않습니다. 프로그램이 두 위치에서 계정을 역직렬화할 때 각 역직렬화는 별도의 인메모리 객체를 생성합니다. 한 객체에 대한 변경은 다른 객체에 영향을 미치지 않습니다. 동일한 계정이 두 번 전달되면, 두 객체는 동일한 온체인 데이터에서 시작하지만 어느 하나가 변경되는 순간 서로 달라집니다.

핸들러가 종료되면, 프로그램의 종료 로직(예: Anchor의 AccountsExit)은 각 계정 객체를 고정된 순서로 계정의 데이터에 직렬화합니다. 두 객체가 동일한 계정에 매핑되면, 두 번째 직렬화가 첫 번째 직렬화를 완전히 덮어씁니다.

취약점 분석

영향을 받은 Allbridge Core 배포의 정확한 소스 코드는 이용할 수 없었습니다. 분석은 ProgramData에 저장된 1,770,736바이트 ELF(SHA-256: 40f776...346bb6)로부터 재구성되었습니다. 마지막 배포 슬롯(204,727,029)은 공격 슬롯(433,941,722)보다 앞서 있습니다.

버그가 있는 프로그램은 BrdgN2...ceWB입니다. 공격 트랜잭션의 Swap 명령(명령 3~7)에서 송신 및 수신 계정 쌍 네 개가 동일했습니다:

역할 계정
send_mint / receive_mint Es9vMF...wNYB
send_pool / receive_pool DW4a2E...wCX
send_bridge_token / receive_bridge_token 2xY9TD...vohV
send_user_token / receive_user_token 817UdW...CVct

파서에는 각 역할을 예상 민트, 볼트, 소유자, PDA, 권한, 또는 Token 프로그램에 바인딩하는 16개의 복구된 32바이트 비교가 포함되어 있습니다. 그러나 send_pool.key() != receive_pool.key()를 강제하는 비교는 없습니다. 두 Pool 역할은 핸들러가 실행되기 전에 독립적으로 역직렬화됩니다.

배포된 ELF는 두 개의 Pool 구성, 송신 및 수신 계산, 그리고 고정된 순서의 두 개의 Pool 종료를 보여줍니다:

L54249-L54252  function_9881(accounts[5])     -> send_pool local
L54294-L54297  function_9881(accounts[6])     -> receive_pool local

L74177-L74195  function_12623(send_pool)      -> swap_to_v_usd
L74368-L74389  function_12940(receive_pool)   -> swap_from_v_usd

L55932-L55936  function_8300(send_pool)       -> 전체 Pool 직렬화
L55940-L55944  function_8300(receive_pool)    -> 전체 Pool 직렬화

function_9881 호출은 별도의 176바이트 로컬 Pool 객체를 할당하고 디코딩된 필드를 그 안에 복사합니다. 계정 키가 동일할 때, 두 객체는 동일한 데이터에서 시작하지만 송신 객체를 변경해도 수신 객체는 변경되지 않습니다.

재구성된 의사 Rust 코드(소스 복구가 아닌 배포 동작) [2] [3]:

let mut send_pool = Account::<Pool>::try_from(&accounts[5])?;
let mut receive_pool = Account::<Pool>::try_from(&accounts[6])?;

require_keys_eq!(send_pool.mint, send_mint.key());
require_keys_eq!(receive_pool.mint, receive_mint.key());

// 볼트, 소유자, PDA, 권한, Token 프로그램 바인딩이 확인됩니다.
// 누락: require_keys_neq!(send_pool.key(), receive_pool.key());

token::transfer(send_user_token, send_bridge_token, amount)?;
let v_usd = swap_to_v_usd(&mut send_pool, amount)?;
let receive_amount =
    swap_from_v_usd(&mut receive_pool, v_usd, receive_amount_min)?;
token::transfer(receive_bridge_token, receive_user_token, receive_amount)?;

// 핸들러 반환 후 AccountsExit:
send_pool.exit(program_id)?;     // 첫 번째 쓰기
receive_pool.exit(program_id)?;  // 두 번째 쓰기, 첫 번째를 덮어씀

function_8300은 위치 0에서 기록을 시작하여 정의된 131개의 Pool 바이트를 모두 직렬화합니다. 첫 번째 종료는 send_pool을 기록하고, 두 번째 종료는 동일한 바이트 위에 receive_pool을 덮어씁니다. 따라서 최종 계정 상태는 두 로컬 업데이트의 병합이 아닌 오래된 수신 측 값이 됩니다.

SPL 토큰 전송은 별도의 CPI입니다. Pool 종료 이전에 SPL Token 프로그램이 소유한 볼트 계정을 업데이트하므로, 두 번째 Pool 직렬화는 입력 전송을 되돌릴 수 없습니다. 따라서 각 별칭 스왑은 전송된 토큰을 볼트에 남겨두면서 대응하는 송신 측 Pool 업데이트를 폐기합니다. 수신 측 업데이트는 유지되어 기록된 Pool 상태를 더 높은 v_usd_balance와 더 낮은 토큰 측 회계 방향으로 이동시킵니다.

핵심 결함은 프로그램이 send_poolreceive_pool이 서로 다른 계정인지 확인하지 않는다는 것입니다. 두 역할이 동일한 Pool에 합법적으로 속하기 때문에, 다른 모든 유효성 검사(민트 바인딩, 볼트 소유권, PDA 유도)는 통과됩니다.

공격 분석

다음 분석은 트랜잭션 3LNLaG...Y39Q를 기반으로 합니다.

익스플로잇은 Kamino 플래시 차입, 일곱 개의 Allbridge Swap 명령, Kamino 상환이 포함된 하나의 트랜잭션으로 완료되었습니다.

  • 1단계: 공격자는 Kamino에서 ~1.12M USDC를 차입하고 Allbridge를 통해 ~949K USDT로 스왑했습니다(명령 1-2). 이 첫 번째 스왑은 이후 셀프 스왑에 필요한 USDT를 공급하고 USDCUSDT Pool 상태를 변화시켰습니다.
  • 2단계: 공격자는 송신 및 수신 모두에 동일한 민트, Pool, 볼트, 사용자 토큰 계정을 사용하여 다섯 번의 ~100K USDT 셀프 스왑을 제출했습니다(명령 3-7). 매 호출마다 수신 Pool이 마지막으로 직렬화되었기 때문에, 지속된 회계는 수신 측 감소만 기록하고 송신 측 추가는 기록하지 않았습니다. 왜곡이 누적되면서 관찰된 출력은 각 호출마다 감소했습니다:
셀프 스왑 입력 기록된 vUSD 가격 (vUSD/USDT) USDT 출력
1 ~100K ~162K ~5.24 ~47.8K
2 ~100K ~256K ~16.6 ~25.4K
3 ~100K ~471K ~57.2 ~12.5K
4 ~100K ~918K ~190 ~5.73K
5 ~100K ~1.82M ~563 ~2.36K

다섯 번의 호출로 ~500K USDT가 볼트에 전송되고 ~93.8K USDT가 반환되었습니다. Pool의 기록된 token_balance는 각 호출마다 감소한 반면 실제 볼트 잔액은 증가하여, 다음 단계가 악용하는 격차가 벌어졌습니다.

  • 3단계: 공격자는 단 ~3.99K USDT만 공급했습니다(명령 8). 왜곡된 USDT Pool은 ~2.24M vUSD를 생성하였고, USDC Pool은 이를 ~2.24M USDC로 전환했습니다. 다섯 번의 송신 측 업데이트 폐기 이후 Pool의 기록된 토큰 잔액이 실제 볼트 잔액보다 훨씬 낮았기 때문에 부풀려진 vUSD 출력이 가능했습니다.
  • 4단계: 공격자는 ~1.12M USDC Kamino 플래시 론 원금과 ~11.2 USDC 수수료를 상환했습니다(명령 9). 최종 공격자 잔액은 ~1.12M USDC와 ~539K USDT였습니다.

결론

이 사고의 근본 원인은 send_poolreceive_pool이 서로 다른 계정을 참조하는지 확인하는 유효성 검사가 누락된 것이었습니다. 단일 가변 Pool이 두 개의 로컬 회계 객체로 역직렬화되었고, Token 프로그램이 볼트 전송을 이미 완료한 후 두 번째 전체 직렬화가 첫 번째를 덮어썼습니다. 이로 인해 Pool의 기록된 회계와 실제 볼트 잔액이 분리되었습니다. 계정 별칭, ELF 제어 흐름, 직렬화 순서, 트랜잭션 로그, 볼트 잔액 모두 동일한 메커니즘을 지지합니다 [1].

Solana 프로그램의 경우, 동일한 계정 유형을 두 개 이상의 가변 역할로 받아들이는 명령은 고유성을 강제(require_keys_neq!)하거나 종료 전에 변경 사항을 병합해야 합니다. Anchor 프레임워크의 #[account] 제약 시스템은 기본적으로 역할 간 고유성을 강제하지 않으므로, 이 검사를 명시적으로 추가해야 합니다. 일반적인 패턴인 두 번째 쓰기가 첫 번째 쓰기를 조용히 덮어쓰는 동일한 상태에 대한 두 개의 가변 뷰는 프로그램이 자체 직렬화 순서를 관리하는 모든 곳에서 나타날 수 있습니다.

참고 자료

Phalcon Explorer 시작하기

트랜잭션을 심층 분석하여 현명하게 행동하세요

지금 무료로 사용해보기

이번 주 추가 사고

Zilliqa Ledger 지갑

2026년 7월 20일, Zilliqa는 레거시 네이티브 ZIL 계정에 대한 활성 익스플로잇과 일치하는 온체인 활동을 확인했으며, 알려진 손실은 약 $400K입니다. 근본 원인은 Zilliqa Ledger 앱의 EC-Schnorr 서명 경로에 있는 편향된 논스였습니다: 논스 생성 코드가 모듈러 감산 후 40바이트 버퍼에서 잘못된 32바이트를 복사하여, 최상위 64비트를 0으로 고정시켰습니다. 동일한 계정에서 여러 공개 서명이 있으면, 공격자는 격자 기반 기법을 통해 개인 키를 복구하고 계정을 탈취할 수 있습니다.

배경

Zilliqa 네이티브 트랜잭션은 secp256k1에서 EC-Schnorr 서명을 사용합니다. 각 서명에 대해 서명자는 0k<N0 \le k < N(secp256k1 곡선 차수)을 만족하는 신선하고 전폭적이며 예측 불가능한 임시 논스 kk를 샘플링해야 합니다. 서명 흐름은 커밋먼트 Q=compress(kG)Q = \text{compress}(kG), 챌린지 r=SHA256(QpublicKeymsg)Nr = \text{SHA256}(Q \| \text{publicKey} \| \text{msg}) \bmod N, 그리고 응답 s=krxNs = k - r \cdot x \bmod N을 생성하며, 여기서 xx는 개인 키입니다. 브로드캐스트 후 (r,s)(r, s)는 공개됩니다.

선형 관계 s=krxNs = k - rx \bmod N이 핵심입니다. 올바른 구현은 모든 kk가 신선하고 균등하게 분포되어 있기 때문에 안전을 유지합니다. 논스가 편향되거나 너무 작으면, 각 공개 서명은 동일한 개인 키에 대한 정보를 유출합니다.

취약점 분석

관련 논스 생성 경로는 Zilliqa Ledger 앱 커밋 "Ledger 팀 검토 기반 버그 수정"에서 도입되었습니다. 의도된 흐름은 40바이트의 난수를 생성하고, 곡선 차수 NN으로 모듈러 감산하고, 결과 스칼라를 kk로 사용하는 것이었습니다. 폭넓은 난수를 생성하고 곡선 차수로 모듈러 감산하는 것 자체는 문제가 없습니다. 결함은 논스 버퍼로의 복사 과정에서 나타났습니다:

unsigned char nonce[size+8];
cx_rng(nonce, size+8);
cx_math_modm(nonce, size+8, (unsigned WIDE char *) PIC(domain->n), size);
os_memcpy(T->K, nonce, size);

cx_math_modm 이후, 32바이트 스칼라 결과는 40바이트 버퍼 내에서 오른쪽 정렬됩니다. os_memcpy(T->K, nonce, size)는 처음 size(32) 바이트를 복사하므로, 앞의 8개 제로 패딩 바이트를 유지하고 엔트로피의 마지막 8바이트를 버립니다. 따라서 생성된 논스는 k<N2256k < N \approx 2^{256} 대신 k<2192k < 2^{192}를 만족합니다.

이는 모든 논스의 64비트가 0으로 고정됨을 의미합니다. Schnorr 응답 방정식 ki=si+rixNk_i = s_i + r_i x \bmod N으로부터, 각 서명은 결과가 21922^{192}로 제한되는 공개 모듈러 선형 관계를 제공합니다. 이는 은닉 수 문제(HNP, Hidden Number Problem) 인스턴스입니다. 동일한 키에서 약 네 개 이상의 영향받은 서명이 있으면, 표준 격자 감산 알고리즘이 일반 하드웨어에서 수 초 내에 개인 키를 복구합니다 [1].

이 결함은 2019년부터 2026년 7월 사고가 확인될 때까지 Zilliqa Ledger 앱의 모든 출시 버전에 존재했습니다 [2].

이 사고에 대한 공격 분석은 제공되지 않습니다. 익스플로잇은 완전히 오프체인에서 수행되었습니다: 공격자는 격자 감산을 사용하여 공개적으로 이용 가능한 온체인 서명에서 개인 키를 복구한 후, 표준 전송 트랜잭션에 서명했습니다. 분석할 다단계 온체인 공격 시퀀스가 없습니다.

결론

이 사고는 온체인 스마트 컨트랙트 버그가 아닌 오프체인 서명 구현 결함으로 인해 발생했습니다. Zilliqa Ledger 앱은 k<2192k < 2^{192}로 제약된 논스를 가진 EC-Schnorr 서명을 생성하여, 동일한 계정에서 여러 번의 네이티브 트랜잭션 후 개인 키 복구를 위한 충분한 구조화된 정보를 유출했습니다.

주요 완화책은 단순히 Ledger 앱을 업데이트하는 것이 아니라 키를 폐기하는 것입니다. 수정된 앱은 새로운 취약한 서명을 방지하지만, 이미 온체인에 기록된 공개 서명을 지울 수는 없습니다. 충분한 수의 취약한 서명을 생성한 영향받은 키는 탈취된 것으로 간주해야 합니다 [2].

지갑 및 프로토콜 팀을 위해: 논스 생성과 인코딩을 보안에 중요한 코드로 취급하고, 해당하는 경우 잘 검토된 표준을 따르는 결정론적 논스 생성을 선호하며, 영향받은 사용자에게는 이미 동일한 개인 키를 보유한 공격자에 의한 프런트런닝을 고려한 조율된 마이그레이션 경로를 제공하세요.

참고 자료


Wanchain Cardano 브리지

2026년 7월 20일, Wanchain Cardano 브리지는 Cardano TreasuryCheck Plutus 검증기의 비단사적 메시지 인코딩으로 인해 약 515.2M NIGHT(~$500K)를 손실했습니다 [1]. 프로젝트의 사후 분석 보고서 [2]는 고수준 메커니즘을 설명하지만 바이트 수준의 인코딩 세부 사항이나 코드를 제공하지 않습니다. 아래 분석은 전체 충돌을 재구성합니다. 브리지 노드 서명은 유효하고 올바르게 검증되었지만, 가변 길이 필드의 구분자 없는 연결로 인해 공격자가 두 인접 숫자 필드 간의 바이트 경계를 이동시켜 3,097.56 NIGHT에 대한 승인을 203,001,692.164714 NIGHT 인출로 재해석할 수 있었습니다.

배경

영향받은 구성 요소는 Wanchain의 Cardano 크로스체인 트레저리 시스템입니다. 소스 체인 사용자가 토큰을 소각하거나 잠그면, 브리지 노드가 파싱된 리디머 필드로부터 구성된 승인 메시지에 서명하고, 서명된 증명이 Cardano TreasuryCheck Plutus 검증기에 제출되어 트레저리에서 자산이 해제됩니다.

취약점 분석

TreasuryCheck 검증기는 14개의 파싱된 리디머 필드의 원시 연결에 대한 브리지 노드 서명을 검증합니다:

hashRedeemer = sha3_256 $ mconcat
  [ toPkhPay, toPkhStk, policy, assetName
  , packInteger amount, packInteger adaAmount
  , txHash, packInteger index, packInteger mode
  , uniqueId, packInteger txType, packInteger ttl
  , packInteger outputCount, userData
  ]

packInteger가 사용하는 정수 인코딩은 가변 길이이며, 연결에는 길이 접두사, 유형 구분자, 또는 도메인 분리된 타입 직렬화가 없습니다. 따라서 서로 다른 의미론적 튜플이 동일한 서명된 바이트 문자열을 생성할 수 있습니다.

대표적인 공격에서, 브리지 노드는 정상적인 인출에 서명했습니다:

amount    = 3097560000     -> packInteger = b8a103c0
adaAmount = 1206800        -> packInteger = 126a10
combined                                  = b8a103c0126a10

공격자는 다음과 같이 파싱된 Cardano 리디머를 제출했습니다:

amount    = 203001692164714 -> packInteger = b8a103c0126a
adaAmount = 16              -> packInteger = 10
combined                                   = b8a103c0126a10

바이트 시퀀스는 동일합니다. 서명 검사가 통과되었고, Cardano 컨트랙트는 리디머를 승인된 것보다 약 65,000배 큰 인출로 해석했습니다.

공격 분석

다음 분석은 대표적인 Cardano 트랜잭션 0a4861...2ea1과 BSC 소스 트랜잭션 0xe90111...d5f26b을 참조합니다.

  • 1단계: 공격자는 정상적으로 보이는 소스 체인 브리지 요청을 생성했습니다. 대표적인 경우, BSC 트랜잭션은 3,110 NIGHT를 소각하였고, Wanchain API는 예상 Cardano 수신 금액을 3,097.56 NIGHT로 기록했습니다 [3].

  • 2단계: 브리지 노드가 원시 연결 필드로부터 구성된 승인 해시에 서명했습니다.

  • 3단계: 공격자는 서명된 바이트 시퀀스를 변경하지 않으면서 amountadaAmount 사이의 Cardano 리디머 경계를 변경했습니다.

  • 4단계: Cardano TreasuryCheck 검증기가 리디머를 고액 인출로 파싱하고 재사용된 서명을 성공적으로 검증했습니다. 트랜잭션은 공격자에게 203,001,692.164714 NIGHT를 지불했습니다.

  • 5단계: 동일한 패턴이 여러 Cardano 트랜잭션에 걸쳐 반복되어 총 약 515,206,545.426856 NIGHT(~$500K)가 탈취되었습니다.

결론

어떠한 암호화도 해독되지 않았습니다. 서명자들은 유효한 서명을 생성했고, TreasuryCheck 검증기는 이를 올바르게 검증했습니다. 결함은 메시지 구성에 있었습니다: hashRedeemer가 14개의 가변 길이 필드를 구분자나 길이 접두사 없이 하나의 바이트 문자열로 접었기 때문에 인코딩이 비단사적이 되었습니다. 서로 다른 필드 튜플이 동일한 원상, 동일한 해시, 동일한 유효한 서명을 생성합니다.

수정책은 서명된 인코딩을 단사적으로 만들어 정확히 하나의 필드 튜플만이 주어진 바이트 문자열을 생성할 수 있게 하는 것입니다. 고정 폭 정수 인코딩이나 각 필드에 길이 접두사를 추가하면 공격자가 이동시킨 경계가 고정됩니다. 표준 구조화된 인코더도 동일한 결과를 달성하며 오류 발생 가능성이 낮습니다. 일반 규칙: 원시 연결이 아닌 정규 직렬화에 서명하세요. 이는 가변 길이 인수와 함께 Solidity의 abi.encodePacked를 사용할 때와 같은 버그 유형으로, 서로 다른 입력 튜플이 동일한 바이트 시퀀스를 생성할 수 있습니다. 이는 가변 길이 필드로 조립된 서명된 메시지에 적용되며, 메시지를 구성하고 파싱하는 당사자들이 서로 다른 시스템에서 운영되는 브리지에서 특히 관련성이 높습니다.

참고 자료


Lien Finance

2026년 7월 24일, Ethereum의 탈중앙화 채권 OTC 프로토콜인 Lien Finance가 USDC 약 $542K를 탈취당했습니다. 근본 원인은 채권 교환 함수의 유효성 검사 결함이었습니다: 입력 및 출력 그룹 간에 공유 채권의 총 수가 일치하는지 확인했지만, 어떤 특정 채권이 매칭되었는지는 추적하지 않았습니다. 이를 통해 공격자는 모든 입력 채권의 소각 단계를 우회하면서 담보 없는 채권을 발행하고, 이를 프로토콜의 OTC 풀을 통해 실제 USDC로 판매할 수 있었습니다.

배경

Lien Finance는 ETH에 대한 담보 채권을 발행하는 DeFi 프로토콜입니다. BondMakerCollateralizedEth 컨트랙트는 사용자가 채권 토큰을 발행하기 위해 ETH를 담보로 잠글 수 있게 합니다. 채권의 수익은 fnMap으로 정의되며, 이는 만기 시 담보 가격을 해당 채권이 지급하는 금액에 매핑하는 구간 선형 함수입니다.

의미 있는 단위는 채권 그룹입니다. registerNewBondGroup()은 그룹 내 모든 채권의 수익이 결합된 fnMap의 모든 분기점에서 ETH 가격의 합산이 되는지 검증합니다. 이 조건을 만족하는 그룹은 정확히 하나의 담보 단위를 재구성합니다. 이것이 그룹이 서로 교환 가능한 이유입니다: 해당 조건을 만족하는 두 그룹은 동일한 가치를 가지며, exchangeEquivalentBonds() 변환 경로는 이 보증에 의존합니다.

채권과 그룹 등록은 모두 허가 없이 가능합니다: 어떤 주소든 만기와 임의의 fnMap을 제공하여 새 채권을 등록할 수 있으며, 어떤 주소든 이미 등록된 채권 ID 목록을 수익 합산 검사만을 조건으로 그룹으로 등록할 수 있습니다.

취약점 분석

버그가 있는 컨트랙트는 0xDA6F...BEf00x8432...7de0입니다.

채권이 입력 그룹과 출력 그룹 모두에 나타날 때, 소각하고 즉시 재발행하는 것은 낭비적인 작업입니다. exceptionBonds 매개변수는 교환이 두 단계를 모두 건너뛸 수 있도록 해당 채권을 명명합니다. 함수는 단일 카운터 exceptionCount로 이를 강제합니다: 입력 그룹을 스캔하면서 매칭마다 한 번 증가하고, 출력 그룹을 스캔하면서 매칭마다 한 번 감소하며, 어떤 bondID가 매칭되었는지는 기록하지 않습니다.

이는 입력 그룹 [BondA, BondB]와 출력 그룹 [BondC, BondA, BondA], exceptionBonds = [BondA, BondB]가 유효성 검사를 통과함을 의미합니다. 카운터는 입력 측에서 2에 도달하고(BondA와 BondB 각각 한 번 매칭) 출력 측에서 0으로 돌아오지만, 두 번의 감소는 모두 BondA의 두 항목에서 나옵니다. 교환은 아무것도 소각하지 않고 BondC만 발행합니다: 소비된 입력 없이 출력 그룹 토큰이 생성됩니다.

공격 분석

공격은 두 개의 트랜잭션으로 나뉩니다: 1단계는 0xe8689a...284d0f에서 발생하고, 2-5단계는 0xb96d57...48e0e7에서 발생합니다.

  • 1단계: 공격자는 허가 없는 registerNewBond()를 세 번 호출하여 동일한 만기를 가진 BondA, BondB, BondC를 정의했습니다. BondA와 BondB는 _assertBondGroup()이 요구하는 것처럼 수익이 ETH 가격의 합산이 되는 $3,200 이하에서 담보를 균등하게 분할하는 레버리지 콜입니다. BondC는 $1,870 현물가에 대해 $3,200에 설정된 외가격 LBT입니다: 내재 가치는 0이지만, 옵션으로서 여전히 IMT당 약 $7.64의 시간 가치가 있습니다.

  • 2단계: 공격자는 두 개의 채권 그룹을 등록했습니다. registerNewBondGroup([BondA, BondB])는 그룹 36(입력)을 반환하고, registerNewBondGroup([BondC, BondA, BondA])는 그룹 37(출력)을 반환했습니다. BondC의 0 수익은 분기점 합산에 아무것도 추가하지 않으므로, 그 존재는 동등 조건을 방해하지 않습니다. BondA가 그룹 37에 두 번 나열된 것이 교환이 유효성 검사를 통과하게 만드는 요소입니다.

  • 3단계: 공격자는 exchangeEquivalentBonds(inputGroup = 36, outputGroup = 37, amount = 6968565865905, exceptionBonds = [BondA, BondB])를 호출했습니다. 입력 그룹 스캔: BondA와 BondB가 각각 예외와 매칭되어 소각을 건너뛰고 exceptionCount가 2에 도달합니다. 출력 그룹 스캔: BondC는 아무것과도 매칭되지 않아 발행되고, BondA의 두 항목이 각각 예외와 매칭되어 카운터를 0으로 감소시킵니다.

  • 4단계: 공격자는 발행된 BondC를 프로토콜의 OTC 풀(GeneralizedDotc)을 통해 USDC로 판매하여 532,144 USDC를 획득했습니다.

  • 5단계: 공격자는 두 번째 BondMakerCollateralizedEth 컨트랙트에 대해 동일한 패턴을 반복하여 총 이익이 약 542,144.63 USDC에 달했습니다.

결론

공유 채권의 재발행을 건너뛰기 위한 exceptionBonds 메커니즘은 출력 그룹에 bondID를 중복시킴으로써 모든 입력 채권에 한 번에 적용될 수 있었습니다. 이는 채권 토큰이 issueNewBonds()를 통해 예치된 담보에 대해서만 생성된다는 불변성을 깨뜨렸습니다. 수정책은 어떤 특정 bondID가 매칭되었는지 추적(예: 비트맵 또는 집합 사용)하여, 각 예외가 두 그룹 모두에서 정확히 한 번씩만 소비되도록 하는 것입니다.

Phalcon Security 시작하기

모든 위협을 탐지하고, 중요한 것을 알리며, 공격을 차단하세요.

지금 무료로 사용해보기

BlockSec 소개

BlockSec은 풀스택 블록체인 보안 및 암호화폐 컴플라이언스 공급업체입니다. 저희는 고객이 프로토콜과 플랫폼의 전체 라이프사이클에 걸쳐 코드 감사(스마트 컨트랙트, 블록체인 및 지갑 포함)를 수행하고, 공격을 실시간으로 차단하며, 사고를 분석하고, 불법 자금을 추적하며, AML/CFT 의무를 충족할 수 있도록 돕는 제품과 서비스를 구축합니다.

BlockSec은 권위 있는 학회에서 다수의 블록체인 보안 논문을 발표하고, DeFi 애플리케이션의 여러 제로데이 공격을 보고하며, 2천만 달러 이상을 구제하기 위한 다수의 해킹을 차단하고, 수십억 달러의 암호화폐를 보안했습니다.

Sign up for the latest updates
~$135만 달러 손실: BarnBridge, DeFiTuna | BlockSec 주간 보고
Security Insights

~$135만 달러 손실: BarnBridge, DeFiTuna | BlockSec 주간 보고

이 주간 보고서는 2026년 7월 13일부터 19일까지 발생한 2건의 보안 사고를 다루며, 이더리움과 솔라나에서 총 약 135만 달러의 손실이 발생했습니다. 솔라나 대출 프로토콜 DeFiTuna은 포지션 가치가 0일 때 부채와 무관하게 건전한 것으로 처리되는 결함으로 약 57만 달러를 잃었습니다. BarnBridge는 더 이상 사용되지 않지만 활성 상태인 이더리움 거버넌스 시스템을 악용한 공격자가 악의적인 제안을 통과시켜 약 77만 6천 달러의 USDC를 탈취당했습니다.

~$800K 손실: Hinkal 이중 지불 사건 | BlockSec 위클리
Security Insights

~$800K 손실: Hinkal 이중 지불 사건 | BlockSec 위클리

이 주간 보안 보고서는 2026년 6월 29일~7월 5일 이더리움에서 발생한 약 80만 달러 손실의 주요 사고 1건을 다룹니다. Hinkal 실드 풀 프로토콜이 이중 지출 공격으로 탈취되었으며, 레거시 노트 형식의 결함을 악용해 단일 예치금에서 다수의 널리파이어를 도출한 것으로 추정됩니다. 회로 수준 취약점, 공격 흐름, 널리파이어 기반 프라이버시 프로토콜의 시사점을 분석합니다.

뉴스레터 - 2026년 6월
Security Insights

뉴스레터 - 2026년 6월

이 월간 보고서는 2026년 6월의 3대 보안 사고를 다루며, 총 확인 손실액은 약 2,200만 달러입니다. 정교한 허니팟 공격으로 미확인 토큰 허용량을 악용해 JaredFromSubway의 MEV 봇에서 약 1,500만 달러가 유출됐습니다. 두 레거시 Aztec 롤업 배포에서 증명 정산 경계 취약점으로 약 435만 달러가 손실됐습니다. SecondFi의 Ed25519 구현 결함으로 지갑 개인키가 노출되어 374개 지갑에서 약 240만 달러가 유출됐습니다. 세 사고 모두 표면상 온전해 보였지만 실제로는 적용되지 않은 보안 보장이라는 공통 패턴을 공유합니다.

Best Security Auditor for Web3

Validate design, code, and business logic before launch. Aligned with the highest industry security standards.

BlockSec Audit