출처: https://cryptomarketpool.com/reentrancy-attack-in-a-solidity-smart-contract/
재진입 공격은 가장 유명한 스마트컨트랙트 공격 방법이다. 실례로는 무려 이더리움 하드포크까지 (이더리움 / 이더리움 클래식) 야기한 The DAO 공격이 있다.
아래와 같은 EtherStore 컨트랙트가 있다. 얘는 일종의 은행이다.
유저가 deposit()으로 저금하면, 잔고가 balances라는 mapping에 저장되는 아주 간단한 구조이다. withdraw()로 한번에 인출도 가능하다.
간단하고 깔끔한 이 컨트랙트에 사람들은 이더를 저금한다.
똑똑하지만 나쁜 공격자는 아래와 같은 Attack 컨트랙트를 만든다.
공격자는 1이더와 함께 이 Attack.sol의 attack()을 호출한다.
require문을 통과하고, etherStore.deposit{value: 1 ether}();
문을 통해 위 EtherStore에 공격자의 이름으로 1이더가 저금된다.
다음으로 etherStore.withdraw()
에 의해 위 EtherStore의 withdraw() 함수가 호출된다.
바로 위에서 공격자의 이름으로 1이더를 저금했으니, 무난하게 require(bal > 0);
을 통과한다.
곧바로 withdraw() 함수 내의 (bool sent, ) = msg.sender.call{value: bal}("");
가 실행된다.
이제 폴백함수의 내용이 실행된다. EtherStore에는 공격자를 제외한 많은 사람들이 이더를 저금해놨고, 그렇기에 EtherStore의 balance는 당연히 1 이상이다. 따라서 폴백함수의 if (address(etherStore).balance >= 1 ether)
조건은 충족되고, etherStore.withdraw()
가 다시 호출된다.
공격자는 분명 위에서 1이더를 인출했다. 그러나 EtherStore의 withdraw()의 balances[msg.sender] = 0;
는 아직 실행되지 않았다. 해당 코드가 실행되기 전 Attack의 폴백함수로 이동했기 때문. 결국 EtherStore는 공격자가 여전히 1이더를 가지고 있다고 판단한다.
잔고가 1이더 밑으로 떨어졌다. 그럼 4번 과정에서의 조건문을 충족하지 못해 withdraw()의 호출이 더이상 발생하지 않는다. 그때서야 withdraw()에 밀려있던 다음 과정, balances[msg.sender]=0;
이 반복해서 우수수 실행된다. 그러나 이미 늦었지..
결국 EtherStore의 이더는 전부 Attack 컨트랙트로 이동된다.