QBridge 익스플로잇 기술 분석
이것은 온체인 증거, 컨트랙트 소스 코드, 그리고 공격 당시 캡처된 스크린샷 증거를 기반으로 한 익스플로잇의 완전한 기술 분석입니다. 피해자 커뮤니티의 중국어 원문 분석을 번역하고, 추출된 모든 데이터와 함께 확장하였습니다.
주요 주소
섹션 제목: “주요 주소”| 역할 | 주소 |
|---|---|
| 공격자 | 0xd01ae1a708614948b2b5e0b7ab5be6afa01325c7 |
| QBridge 컨트랙트 (Ethereum) | 0x20e5e35ba29dc3b540a1aee781d0814d5c77bce6 |
| QBridge 핸들러 (BSC) | 0x4d8ae68fcae98bf93299548545933c0d273ba23a |
| ETH resourceID | 0x0000000000000000000000002f422fe9ea622049d6f73f81a906b9b8cff03b7f01 |
1단계 — BSC는 최초 범행 현장이 아니었다
섹션 제목: “1단계 — BSC는 최초 범행 현장이 아니었다”조사관들이 BSC에서 공격자의 주소를 처음 확인했을 때, 즉시 무언가 이상했습니다:

공격자는 곧바로 borrow()를 실행했습니다 — 준비도, 플래시 론도, 컨트랙트 배포도 없었습니다. 이는 BSC가 공격이 시작된 곳이 아니라는 것을 의미했습니다. 공격자는 렌딩 프로토콜에 접근하기 전에 이미 qXETH 토큰을 보유하고 있었습니다.
해당 qXETH 토큰을 역추적한 결과, 브릿지 릴레이어에 의해 발행되었음이 밝혀졌으며 — 이는 출처가 Ethereum이었음을 의미했습니다.
2단계 — BSC에서의 voteProposal 트랜잭션
섹션 제목: “2단계 — BSC에서의 voteProposal 트랜잭션”qXETH 발행 트랜잭션 중 하나:
트랜잭션: 0x8c5877d1b618f29f6a3622cb610ace08ca96e04d8218f587072a3f91e8545bdc

이 트랜잭션에서의 주요 관찰 사항:
- QBridge BSC 컨트랙트에서
voteProposal(uint8 chainID, uint64 depositNonce, bytes32 resourceID, bytes data)호출 - 3건의 토큰 전송 발생:
- Null 주소 →
0xa6b1bb...— 59,900 Cross-Chain xETH (발행) 0xa6b1bb...→0xfd7a55...— 59,900 xETH (브릿지됨)- Null 주소 → QubitFin Exploiter — 59,900 qXETH (담보용 발행)
- Null 주소 →
- 값: 0 BNB — 실제 ETH는 어디에도 입금되지 않음
voteProposal 함수는 릴레이어만 호출할 수 있었습니다. 릴레이어가 이를 호출한 이유는 Ethereum에서 Deposit 이벤트를 감지했기 때문입니다. 그 이벤트는 위조된 것이었습니다.
3단계 — Ethereum에서의 공격자의 입금 내역
섹션 제목: “3단계 — Ethereum에서의 공격자의 입금 내역”
공격자는 Ethereum QBridge 컨트랙트(0x20e5e35ba29dc3b540a1aee781d0814d5c77bce6)에 deposit()을 여러 차례 호출했습니다:
| 트랜잭션 해시 | 블록 | 값 |
|---|---|---|
0x3dfa33b5... | 14090234 | 0 ETH |
0x58020654... | 14090230 | 0 ETH |
0x501f8541... | 14090230 | 0 ETH |
0xa6282e60... | 14090223 | 0 ETH |
0x94031569... | 14090216 | 0 ETH |
0xbdedb13d... | 14090210 | 0 ETH |
0xeb9f622e... | 14090201 | 0 ETH |
모든 트랜잭션의 ETH 값은 0이었습니다. 메서드: deposit. 각 트랜잭션은 릴레이어가 충실히 처리하는 Deposit 이벤트를 발생시켰습니다.
이 트랜잭션 중 하나의 상세 내역:

- 블록: 14090216
- 타임스탬프: 2022년 1월 27일 오후 9:45:32 UTC
- 발신:
0xd01ae1a708614948b2b5e0b7ab5be6afa01325c7(공격자) - 수신:
0x20e5e35ba29dc3b540a1aee781d0814d5c77bce6(QBridge Ethereum) - 값: 0 Ether ($0.00)
- 호출된 함수:
deposit(uint8 destinationDomainID, bytes32 resourceID, bytes data) - 공격 당시 ETH 가격: $2,425.83
4단계 — 두 개의 함수, 하나의 이벤트
섹션 제목: “4단계 — 두 개의 함수, 하나의 이벤트”QBridge 컨트랙트에는 두 개의 입금 함수가 있었습니다:

depositETH()— ETH의 올바른 경로,msg.value > 0필수deposit()— ERC-20 토큰용으로 설계됨, ETH 불필요
두 함수 모두 정확히 동일한 Deposit 이벤트 타입을 발생시켰습니다. 릴레이어는 Deposit 이벤트를 감지했지만 어떤 함수가 이를 발생시켰는지 구분할 수 없었습니다.
5단계 — 핸들러 코드와 화이트리스트
섹션 제목: “5단계 — 핸들러 코드와 화이트리스트”
QBridgeHandler.deposit() 함수:
function deposit( bytes32 resourceID, address depositer, bytes calldata data) external override { address tokenAddress = _resourceIDToTokenContractAddress[resourceID]; // get token address require(_contractWhitelist[tokenAddress], "not whitelisted"); // line 128: whitelist check // ... ERC20Interface(tokenAddress).safeTransferFrom(depositer, address(this), amount); // line 135}ETH의 resourceID에 대해 컨트랙트는 tokenAddress = 0x0000000000000000000000000000000000000000 (제로 주소)을 반환했습니다.
6단계 — 제로 주소가 화이트리스트에 등록되어 있었다
섹션 제목: “6단계 — 제로 주소가 화이트리스트에 등록되어 있었다”
조사관들이 컨트랙트를 조회했습니다: 제로 주소가 화이트리스트에 등록되어 있었는가?
그렇습니다. 등록되어 있었습니다.
이것은 depositETH()도 ETH의 플레이스홀더로서 제로 주소와 함께 동일한 화이트리스트 메커니즘을 사용했기 때문에 필요했습니다. 그러나 이는 치명적인 부작용을 만들어냈습니다: 제로 주소가 deposit()의 화이트리스트 검사도 통과한 것입니다.

ETH resourceID는 0x0000000000000000000000000000000000000000 — 제로 주소 — 에 매핑되어 있었으며, 이는 resourceIDToTokenContractAddress 조회를 통해 확인되었습니다.
7단계 — EOA 호출은 조용히 성공한다
섹션 제목: “7단계 — EOA 호출은 조용히 성공한다”tokenAddress = 0x0000...0000인 상태에서, 핸들러는 다음을 실행했습니다:
ERC20Interface(tokenAddress).safeTransferFrom(depositer, address(this), amount);제로 주소는 EOA — 컨트랙트 코드가 없는 외부 소유 계정입니다.
EVM에서: 컨트랙트 코드가 없는 주소에 대한 함수 호출은 조용히 성공합니다 — 리버트도, 에러도, 실제 실행도 없습니다.
safeTransferFrom 호출은 “성공”했습니다. 아무것도 이동하지 않았습니다. ETH도, 토큰도 없었습니다. 하지만 코드는 모든 것이 정상인 것처럼 계속 진행되었고 — 정상적인 Deposit 이벤트를 발생시켰습니다.

이 정확한 트릭은 2019년 0x Protocol 보안 업데이트에서 공개적으로 문서화되었습니다. Mound Inc.는 이를 고려하지 않고 2022년에 브릿지를 배포했습니다.
8단계 — Borrow 함수는 정확했지만, 이미 늦었다
섹션 제목: “8단계 — Borrow 함수는 정확했지만, 이미 늦었다”
function borrow(address qToken, uint amount) external override onlyListedMarket(qToken) nonReentrant { _enterMarket(qToken, msg.sender); require(IQValidator(qValidator).borrowAllowed(qToken, msg.sender, amount), "Qore: cannot borrow");
IQToken(payable(qToken)).borrow(msg.sender, amount); qDistributor.notifyBorrowUpdated(qToken, msg.sender);}렌딩 컨트랙트는 정확했습니다 — borrowAllowed()는 담보 가치를 적절히 검사했습니다. 하지만 qXETH 토큰은 온체인에서 정상적으로 보였습니다. 사기는 이미 브릿지에서 발생한 것이었습니다. borrow()가 실행될 때쯤, 공격자는 아무것도 뒷받침하지 않는 정상처럼 보이는 담보를 보유하고 있었습니다.
9단계 — 결정적 증거: 공격 전 매개변수 변경
섹션 제목: “9단계 — 결정적 증거: 공격 전 매개변수 변경”이것은 한 번도 설명된 적 없는 세부 사항입니다.

공격 전, 누군가가 deposit() 함수의 이력을 조사했습니다. 그들이 발견한 것:
2021년 12월 1일 — 정상적인 트랜잭션 (0xc85df3fbfee7a8541e9fd354e98df78dca21ac6be40b929ff5da52ea8eab80de):
- 블록: 13719888
- 발신:
0xbee3971293374d0b4db7bf1654936951e5bdfe5a6 - 수신: QBridge 컨트랙트
0x20e5e35ba29dc3b540a1aee781d0814d5c77bce6 - 전송된 토큰: 0.1 WETH (당시 $241.81)
- 함수: 동일한
resourceID로deposit()호출
2021년 12월에는 해당 resourceID로 deposit()을 호출하면 실제 WETH가 전송되었습니다 — resourceID가 실제 WETH 컨트랙트 주소에 매핑되어 있었기 때문입니다.
2021년 12월과 2022년 1월 28일 사이 어느 시점에 — 소유자 전용 함수가 호출되어 resourceID 매핑을 WETH 컨트랙트 주소에서 제로 주소로 재할당했습니다.

컨트랙트 소유자만이 이 변경을 할 수 있었습니다. 타임락이 필요하지 않았습니다. 공지도 없었습니다. 이 단 하나의 보이지 않는 매개변수 변경이 작동하는 브릿지를 익스플로잇 가능한 브릿지로 만들었습니다.
요약: 7가지 동시 실패
섹션 제목: “요약: 7가지 동시 실패”| 실패 요인 | 영향 |
|---|---|
deposit()과 depositETH()가 동일한 이벤트를 발생시킴 | 릴레이어가 실제 입금과 가짜 입금을 구분할 수 없음 |
| EOA 조용한 성공이 고려되지 않음 | 제로 주소에 대한 safeTransferFrom이 전송 없이 통과함 |
| 제로 주소가 화이트리스트에 등록됨 | EOA 호출이 화이트리스트 검사를 통과함 |
| 감사 없는 프로덕션 배포 | 외부 감사인이 위의 문제를 발견하지 못함 |
| 소유자 함수에 타임락 없음 | resourceID가 조용히, 즉시, 보이지 않게 재매핑됨 |
공격 전 resourceID 재매핑 | 익스플로잇을 가능하게 만든 변경 |
| 릴레이어가 이벤트를 맹목적으로 신뢰 | 오프체인 서비스가 온체인 가치를 검증하지 않고 토큰을 발행함 |
외부 참고 자료
섹션 제목: “외부 참고 자료”- 공격 트랜잭션 (BSC):
0x8c5877d1... - Etherscan의 공격자:
0xd01ae1... - QBridge (Ethereum):
0x20e5e3... - EOA 제로 주소 트릭 (0x Protocol, 2019): blog.0xproject.com
- 중국어 원문 분석: qbt.wiki
결정적 증거 트랜잭션 — 2021년 12월 13일 setResource() 호출
섹션 제목: “결정적 증거 트랜잭션 — 2021년 12월 13일 setResource() 호출”이것이 가장 핵심적인 증거입니다.

트랜잭션: 0xe3da555d506638bd7b697c0bdf7920be8defc9a175cd35bf72fb10bc77167b66
| 필드 | 값 |
|---|---|
| 상태 | ✅ 성공 |
| 블록 | 13797391 |
| 타임스탬프 | 2021년 12월 13일 오후 2:31:21 UTC |
| 발신 | 0xbee3971293374d0b4db7bf1654936951e5bdfe5a6 |
| 수신 | 0x20e5e35ba29dc3b540a1aee781d0814d5c77bce6 (QBridge, Ethereum) |
| 값 | 0 ETH |
| 함수 | setResource(address handlerAddress, bytes32 resourceID, address tokenAddress) |
디코딩된 입력 인수:
| # | 매개변수 | 값 |
|---|---|---|
| 0 | handlerAddress | 0x17B7163cf1Dbb6286262ddc68b553D899B893f526 |
| 1 | resourceID | 0x0000000000000000000000002f422fe9ea622049d6e73f81a906b9b8cff03b7f01 |
| 2 | tokenAddress | 0x0000000000000000000000000000000000000000 |
setResource()는 onlyOwner 함수입니다. Mound Inc. 팀만이 이를 호출할 수 있었습니다.
2021년 12월 13일 — 해킹 45일 전 — 컨트랙트 소유자가 ETH resourceID의 tokenAddress 매핑을 의도적으로 제로 주소로 변경했습니다. 이 호출 전에는 동일한 resourceID가 WETH 토큰 컨트랙트를 가리키고 있었습니다. 이 호출 이후에는 아무것도 가리키지 않게 되었으며 — EOA 조용한 성공 익스플로잇이 가능해졌습니다.
릴레이어만 호출할 수 있음을 보여주는 이전 voteProposal 코드:

function voteProposal(uint8 originDomainID, uint64 depositNonce, bytes32 resourceID, bytes calldata data) external onlyRelayers notPaused { address handlerAddress = resourceIDToHandlerAddress[resourceID]; require(handlerAddress != address(0), "QBridge: invalid handler"); // ... if (proposal._status == ProposalStatus.Passed) { executeProposal(originDomainID, depositNonce, resourceID, data, true); return; } // ... if (proposal._status == ProposalStatus.Inactive) { proposal = Proposal({ status: ProposalStatus.Active, _yesVotes: 0, _yesVotesTotal: 0, _proposedBlock: uint40(block.number) });그리고 모든 것을 바꾼 setResource() 코드:

function setResource(address handlerAddress, bytes32 resourceID, address tokenAddress) external onlyOwner { resourceIDToHandlerAddress[resourceID] = handlerAddress; IQBridgeHandler(handlerAddress).setResource(resourceID, tokenAddress);}qXETH 발행 순서 (BSC 토큰 전송)
섹션 제목: “qXETH 발행 순서 (BSC 토큰 전송)”
공격자는 null 주소로부터 직접 발행된 qXETH를 여러 라운드에 걸쳐 수령했습니다:
| 트랜잭션 해시 | 수량 | 토큰 | 출처 |
|---|---|---|---|
0xd8bba15555... | 999 | qXETH | Null 주소 → 공격자 |
0xf6008ab482... | 499 | qXETH | Null 주소 → 공격자 |
0xcfa4379af6... | 140 | ETH (BEP-20) | 0xb4b778… → 공격자 |
0x61ca8bc28f... | 190 | qXETH | Null 주소 → 공격자 |
0x881a68c9c9... | 0.1 | qXETH | Null 주소 → 공격자 |
0x8c5877d1b6... | 0.1 | qXETH | Null 주소 → 공격자 |
각 “Null 주소 → 공격자” qXETH 발행은 Ethereum에서의 위조된 deposit() 호출에 대응합니다.