본문 바로가기
Hack/Cryptocurrency

[DAO Hacking] Reentrancy Attack 실제 사례 분석

by Becoming a Hacker 2023. 4. 30.
반응형

이전 포스팅에서 Reentrancy Attack에 대한 설명을 드렸는데요. 이번에는 실제 해킹 사례를 분석해보도록 하겠습니다.

 

Reentrancy Attack in Smart Contract

Smart Contract는 블록체인 기술에서 중요한 개념 중 하나로, 계약 체결과 실행을 자동으로 수행할 수 있는 프로그램이며, 블록체인 기술을 활용하여 제 3자의 인증 기관 없이 개인 간 계약이 이루어

hacksms.tistory.com

 

The DAO Project

The DAO(Decentralized Autonomous Organization, 이하 DAO)는 2016년 3월 Ethereum Platform을 기반으로 한 세계 최대 디지털 자산 크라우드 펀딩 프로젝트 였습니다.

 

DAO는 참여자들이 ETH를 투자하여 투자한 만큼의 DAO Token을 받고, 해당 Project가 성공할 경우 자신들이 보유한 DAO Token만큼 수익 배분금을 받게되는 구조를 가지고 있었으며, 이러한 DAO Project는 Smart Contract로 구성되어 Ethereum BlockChain 상에서 작동하고 있었습니다.

 

하지만 The DAO Project의 Smart Contract 코드 내에 Reentrancy Attack 취약점이 존재하였고, 해당 취약점을 악용한 Hacker들이 The DAO의 계좌에 있는 360만 ETH(당시 640억원)를 해킹했습니다.

 

이후, Hacker들이 해당 토큰을 현금화할 수 없도록 Hard Fork가 제안되었고 이를 통해 Ethereum은 2가지로 나눠지게 되었습니다.

  • Ethereum : The DAO Hacking의 역사를 무효화한 Ethereum 
  • Ethereum Classic : The DAO Hacking의 역사를 인정한 Ethererum

해킹 사례 분석 : The DAO Project

 

  • 공격의 아이디어 자체는 매우 심플합니다. 
    • split(환불) 제안
    • split(환불) 실행 (splitDAO)
    • DAO가 보상 출금을 마무리 하기 전에 split(환불) 실행 재호출 (Reentrancy Attack)
 function splitDAO(
 uint _proposalID,
 address _newCurator
 ) noEther onlyTokenholders returns (bool _success) {

Proposal p = proposals[_proposalID];

 // Sanity check

 if (now < p.votingDeadline // has the voting deadline arrived?
 //The request for a split expires XX days after the voting deadline
 || now > p.votingDeadline + splitExecutionPeriod
 // Does the new Curator address match?
 || p.recipient != _newCurator
 // Is it a new curator proposal?
 || !p.newCurator
 // Have you voted for this split?
 || !p.votedYes[msg.sender]
 // Did you already vote on another proposal?
 || (blocked[msg.sender] != _proposalID && blocked[msg.sender] != 0) ) {

 throw;
 }

 // If the new DAO doesn't exist yet, create the new DAO and store the
 // current split data
 if (address(p.splitData[0].newDAO) == 0) {
 p.splitData[0].newDAO = createNewDAO(_newCurator);
 // Call depth limit reached, etc.
 if (address(p.splitData[0].newDAO) == 0)
 throw;
 // should never happen
 if (this.balance < sumOfProposalDeposits)
 throw;
 p.splitData[0].splitBalance = actualBalance();
 p.splitData[0].rewardToken = rewardToken[address(this)];
 p.splitData[0].totalSupply = totalSupply;
 p.proposalPassed = true;
 }

 // Move ether and assign new Tokens
 // 새 토큰을 할당하는 코드로 해당 코드의 작동 방법 분석 필요
 uint fundsToBeMoved = (balances[msg.sender] * p.splitData[0].splitBalance) / p.splitData[0].totalSupply;
 if (p.splitData[0].newDAO.createTokenProxy.value(fundsToBeMoved)(msg.sender) == false) // 공격자가 2번 이상 실행하길 원하는 코드
 throw;

 // Assign reward rights to new DAO
 uint rewardTokenToBeMoved =
 (balances[msg.sender] * p.splitData[0].rewardToken) /
 p.splitData[0].totalSupply;

 uint paidOutToBeMoved = DAOpaidOut[address(this)] * rewardTokenToBeMoved /
 rewardToken[address(this)];

 rewardToken[address(p.splitData[0].newDAO)] += rewardTokenToBeMoved;
 if (rewardToken[address(this)] < rewardTokenToBeMoved)
 throw;
 rewardToken[address(this)] -= rewardTokenToBeMoved;

 DAOpaidOut[address(p.splitData[0].newDAO)] += paidOutToBeMoved;
 if (DAOpaidOut[address(this)] < paidOutToBeMoved)
 throw;
 DAOpaidOut[address(this)] -= paidOutToBeMoved;

 // Burn DAO Tokens
 Transfer(msg.sender, 0, balances[msg.sender]);
 
 // 중요 포인트
 withdrawRewardFor(msg.sender); // be nice, and get his rewards
 
 // 자산 상태 업데이트
 totalSupply -= balances[msg.sender];
 balances[msg.sender] = 0;
 paidOut[msg.sender] = 0;
 return true;
 }

 

  • split 되는 토큰의 양을 의미하는 fundsToBeMoved 변수는 actualBalance() 함수를 통하여 현재 사용자의 자산을 기반으로 계산됩니다.
  • 그러나 자산 상태는 split 실행이 마무리 된 이후에 업데이트 되고 있어, 만약 공격자가 자산이 업데이트 되기 이전에 splitDAO를 실행할 수 있을 경우 매번 동일한 양의 토큰을 이동시킬 수 있을 것으로 보입니다.
 function actualBalance() constant returns (uint _actualBalance) {
 return this.balance - sumOfProposalDeposits;
 }
 
 Proposal p = proposals[_proposalID];
 p.splitData[0].newDAO = createNewDAO(_newCurator);
 p.splitData[0].splitBalance = actualBalance();
 p.splitData[0].rewardToken = rewardToken[address(this)];
 p.splitData[0].totalSupply = totalSupply;
  
 // _proposalID를 통하여 계산
 uint fundsToBeMoved = (balances[msg.sender] * p.splitData[0].splitBalance) / p.splitData[0].totalSupply;

 

  • 그러면 보상을 실제로 출금하는 withdrawRewardFor() 함수를 보다 자세히 살펴보도록 하겠습니다.
    • withdrawRewardFor() 함수에서 rewardAccount(Owner Account)를 통하여 payOut() 함수를 호출 함
    • payOut() 함수에서는 Reentrancy 공격에 취약한 call() 함수를 통하여 rewardAccount의 자산을 사용자에게 입금함
 function withdrawRewardFor(address _account) noEther internal returns (bool _success) {
 if ((balanceOf(_account) * rewardAccount.accumulatedInput()) / totalSupply < paidOut[_account])
 throw;

 uint reward =
 (balanceOf(_account) * rewardAccount.accumulatedInput()) / totalSupply - paidOut[_account];
 
 if (!rewardAccount.payOut(_account, reward)) // 취약 포인트
 throw;
 paidOut[_account] += reward;
 return true;
 }
 
 function payOut(address _recipient, uint _amount) returns (bool) {
 if (msg.sender != owner || msg.value > 0 || (payOwnerOnly && _recipient != owner))
 throw;
 if (_recipient.call.value(_amount)()) { // Reentrancy Attack
 PayOut(_recipient, _amount);
 return true;
 } else {
 return false;
 }
 }

 

  • 즉, 실제 The DAO Project의 해킹은 아래와 같은 흐름으로 발생할 수 있습니다.
    1. split 제안 (CreatePorposal 함수)
    2. split 실행 (splitDAO 함수)
    3. The DAO에서 새로운 DAO 토큰을 생성함 (createTokenProxy 함수)
    4. 자산 업데이트 이전에 The DAO에서 공격자에게 보상을 지급함 (withdrawRewardFor 함수 -> payOut 함수)
    5. 4번 과정에서 _recipient.call.value(_amount)()를 통해 공격자에게 흐름이 넘어오게 되면, 공격자는 다시 splitDAO를 실행하여 Reentrancy Attack를 수행함

참조 문서

 

Analysis of the DAO exploit

This post describes how the hacker who took $50+M from The DAO did it.

hackingdistributed.com

 

TheDAO Token | Address 0xbb9bc244d798123fde783fcc1c72d3bb8c189413 | Etherscan

The Contract Address 0xbb9bc244d798123fde783fcc1c72d3bb8c189413 page allows users to view the source code, transactions, balances, and analytics for the contract address. Users can also interact and make transactions to the contract directly on Etherscan.

etherscan.io

 

댓글