sherlock-2024-01-arcadia-m02
[M-02] CREATE2 address collision against an Account will allow complete draining of lending pools
Summary
Account는 각 유저가 Arcadia에서 활동하기 위해 배포하는 컨트랙트로, 담보 토큰을 Account 컨트랙트에 예치하고 대출을 받을 수 있다. 담보 토큰의 양은 balanceOf의 호출 결과가 아니라 erc20Balances 변수로 추적된다.
Account 컨트랙트는 CREATE2로 배포하며 솔트값은 유저가 컨트롤할 수 있는 값이다. meet-in-the-middle 공격으로 주소 충돌을 찾으면 공격자가 동일 주소에 악성 컨트랙트를 배포 후 토큰을 approve 해둘 수 있다. 악성 컨트랙트를 selfdestruct 한 뒤 동일 주소로 Account 컨트랙트를 배포하면 공격자는 실제 담보 토큰은 예치되지 않았지만 담보를 가지고 있는 것으로 취급되는 무한 담보 Account를 가질 수 있다.
Keyword
hash collision, address collision, bruteforce, crypto theft, create2, lending protocol
Vulnerability
createAccount 함수는 Account 컨트랙트를 배포한다. 이 때 CREATE2 를 사용하고, 유저의 입력값과 tx.origin을 이용하여 솔트를 만든다. tx.origin도 유저 쪽에서 조절할 수 있으므로 솔트는 유저가 컨트롤할 수 있다고 볼 수 있다.
@> function createAccount(uint256 salt, uint256 accountVersion, address creditor)
external
whenCreateNotPaused
returns (address account)
{
account = address(
@> new Proxy{ salt: keccak256(abi.encodePacked(salt, tx.origin)) }(
versionInformation[accountVersion].implementation
)
);
...
}Account는 각 유저가 Arcadia에서 활동하기 위해 배포하는 컨트랙트로, 담보 토큰을 Account 컨트랙트에 예치하고 대출을 받을 수 있다. 담보 토큰의 양은 balanceOf의 호출 결과가 아니라 erc20Balances 변수로 추적된다.
function _depositERC20(address from, address ERC20Address, uint256 amount) internal {
ERC20(ERC20Address).safeTransferFrom(from, address(this), amount);
uint256 currentBalance = erc20Balances[ERC20Address];
if (currentBalance == 0) {
erc20Stored.push(ERC20Address);
}
unchecked {
@> erc20Balances[ERC20Address] = currentBalance + amount;
}
}공격자가 Factory에 요청하여 배포할 수 있는 Account 컨트랙트의 주소와 동일한 주소에 임의의 컨트랙트를 배포할 수 있다면 어떨까? 공격 컨트랙트를 배포하고 미리 approve를 호출한 뒤 컨트랙트를 삭제한다면, 동일한 주소에 다시 Account 컨트랙트를 배포할 수 있다. approve를 해둔다면 Account에 예치한 담보 토큰을 빼낼 수 있다. balanceOf의 호출 결과가 아니라 예치 시점에 업데이트된 erc20Balances로 담보의 양을 추적하기 때문에 담보 토큰을 출금해도 여전히 담보로 잡힐 것이다.
즉, 공격의 목적은 주소 충돌을 찾는 것이다. 이를 위해서는 다음을 알아내야 한다.
- 아직 배포되지 않은, 공격자가 배포할 수 있는 Arcadia Account 주소(솔트값을 주면 배포될 주소)
- 공격자가 임의의 컨트랙트를 배포할 수 있는 주소, (1)과 동일해야 함
두 주소는 브루트포스를 통해 찾을 수 있다.
- 많은 솔트를 대입하여 배포되지 않은 Account 주소를 나열할 수 있다.
- 역시 브루트포스를 통해 나열할 수 있다.
공격자는 Meet-In-The-Middle 공격을 사용해 (1)과 (2) 사이의 주소 충돌을 높은 확률로 찾을 수 있다.
- 충분한 수의 솔트 값(2^80)을 브루트포스하고, 미리 계산된 Account 주소를 효율적으로 저장한다. (ex. 블룸필터를 사용하는 등)
- 브루트포스를 통해 공격자가 임의의 컨트랙트를 배포할 수 있는 주소가 1단계에서 저장해 둔 주소와 충돌하는 지 찾는다.
충돌을 찾는데 필요한 자세한 기술 및 하드웨어 요구 사항, 실현 가능성은 다음 참고 문헌에 설명되어 있다.
이 이슈를 제출한 시점 기준 BTC 네트워크의 해시레이트는 초당 6 x 10^20 해시에 도달했으며, 이를 기준으로 2^80 해시를 브루트포스하는 데 33분밖에 걸리지 않는다. 이 컴퓨팅 파워의 일부만으로도 충분히 짧은 시간 내에 충돌을 찾을 수 있다.
해시 충돌 확률 계산기를 통해 계산하면 2^81 해시를 검색했을 때 86%, 2^82 해시를 검색했을 때 99.96%의 확률로 해시 충돌을 발견할 수 있다.
공격: 대출 풀 토큰 탈취
공격자가 충돌되는 주소를 찾아내었고 이 주소는 0xCOLLIDED 라고 하자. 다음 단계를 통해 대출 풀에서 토큰을 탈취할 수 있다.
0xCOLLIDED주소에 공격 컨트랙트를 배포한다.0xCOLLIDED→ 공격자 주소로 토큰 이동을 허용한다.selfdestruct을 통해 컨트랙트를 파기한다. (Dencun 이후에는 생성자에서 모두 해야 한다.)
이제 공격자는 0xCOLLIDED 주소에 들어오는 토큰을 가져갈 수 있다.
다음 트랜잭션으로 자금을 탈취한다.
- Factory에 요청하여 Account 컨트랙트를
0xCOLLIDED에 배포한다. - Asset 토큰을 예치하고, 담보로 한 뒤 위에서 허용했던 공격자 계정을 이용해 담보를 꺼내간다.
- 2번 단계를 반복하여 규모를 키운다. 실제로 토큰은 없지만 예치한 담보 기록은 계속 증가한다. 즉, 무한 담보를 가진 것과 마찬가지가 된다.
- 무한 담보를 이용하여 대출 풀의 모든 토큰을 대출하고 도망가면 실제 자금은 남아있지 않은 무한 담보 계좌만 남게 된다.
즉, 공격자는 대출 풀의 모든 토큰을 꺼내갈 수 있다.
Impact
주소 충돌을 발견한다면 대출 풀의 토큰을 완전히 탈취할 수 있다.
Mitigation
- 유저가 제공한 솔트를 이용하지 않는다. 사용자의 주소 역시 솔트로 사용하지 않아야 한다.
- CREATE2가 아닌 CREATE(create1)을 이용한다. 배포될 컨트랙트의 주소는 팩토리 컨트랙트와 팩토리 컨트랙트의 nonce를 통해 결정될 것이다.
이렇게 하면 브루트포스를 방지하여 공격을 막을 수 있다.
Memo
주소 충돌 이슈에 자주 인용되는 보고서이다. 해시 충돌은 증명하기 어렵고 실현 가능성이 낮아 잘 인정되지는 않지만 어떻게 공격하는 지 알아두면 아이디어를 낼 때 도움이 될 것 같다.
tags: bughunting, arcadia, smart contract, solidity, solo issue, bruteforce, collision, crypto theft, solidity create2, erc3607, lending protocol, severity medium