区块链安全—详谈合约攻击(三)
字数 1196 2025-08-22 18:37:15
区块链安全:智能合约Call函数攻击详解
一、Call函数基础
1. Call函数定义
call()是Solidity中的一个底层接口,用于向合约发送消息。基本语法为:
<address>.call(...) returns (bool)
2. 调用方式
- 直接调用:
test.call("abc", 123)test:调用此call方法的用户abc:调用的方法名称123:传入的参数值
- 字节调用:
<address>.call(bytes)
3. 特性
- 支持传入任意类型的任意参数
- 将参数打包成32字节并拼接后发送
- 调用过程中会修改
msg全局变量msg.sender变为调用者地址- 执行环境为被调用者的运行环境
二、Call函数安全风险
1. 参数处理问题
Solidity的call函数会自动忽略多余参数:
function test(uint256 a) public {
aa = a;
}
function callFunc() public {
this.call(bytes4(keccak256("test(uint256)")), 10, 11, 12); // 仍然能成功调用
}
2. 权限绕过模型
典型身份验证函数:
function isAuth(address src) internal view returns (bool) {
return (src == address(this) || src == owner);
}
攻击方式:
通过call函数调用内部函数,使msg.sender变为合约地址,绕过验证:
function callFunc(bytes data) public {
this.call(data); // msg.sender变为合约地址
}
function withdraw(address addr) public {
if(!isAuth(msg.sender)) throw;
addr.transfer(this.balance);
}
攻击者构造data为withdraw(攻击者地址)即可绕过验证。
3. 代币方法器注入攻击
当只能控制函数名参数时:
function CPcall(address _to, uint _value, bytes data, string fallback){
assert(_to.call(bytes4(keccak256(fallback)),msg.sender, _value, _data))
}
攻击者设置fallback为"transfer",即使参数数量不匹配也能执行转账函数。
三、实际攻击案例:ATN代币增发
1. 漏洞背景
ATN代币合约同时使用了ERC223和ds-auth库,单独使用时安全,但组合使用时出现漏洞。
2. 关键代码
ERC223转账函数:
function transferFrom(address _from, address _to, uint256 _amount, bytes _data, string _custom_fallback) public returns (bool) {
// ...省略验证代码...
if (isContract(_to)) {
ERC223ReceivingContract receiver = ERC223ReceivingContract(_to);
receiver.call.value(0)(bytes4(keccak256(_custom_fallback)), _from, _amount, _data);
}
// ...
}
ds-auth权限验证:
function isAuthorized(address src, bytes4 sig) internal view returns (bool) {
if (src == address(this)) {
return true;
} else if (src == owner) {
return true;
}
// ...
}
3. 攻击步骤
-
攻击者调用
transferFrom(),设置:_from:攻击者钱包地址_to:ATN合约地址_custom_fallback:"setOwner"
-
合约执行call调用时:
msg.sender变为合约本身地址- 绕过
isAuthorized验证 - 成功将owner改为攻击者地址
-
攻击者完成非法操作后,再次调用
setOwner还原权限
四、防御建议
1. 开发原则
- 简单性:使用尽可能少的操作码和数据类型
- 确定性:确保规范无歧义,明确计算步骤和Gas消耗
- 空间节省:保持EVM组件紧凑
- 安全性:易于计算智能合约运行的燃料成本
2. 具体措施
- 避免直接使用原始call函数,使用封装的安全调用方法
- 对用户输入的函数名和参数进行严格验证
- 实现多重权限验证机制,不单纯依赖
msg.sender - 使用最新版本的Solidity编译器,启用所有安全检查
- 对关键函数添加事件日志,便于监控异常调用
3. 安全编码示例
// 安全的调用方式
function safeCall(address target, bytes data) internal {
require(target != address(0), "Invalid target address");
require(data.length >= 4, "Invalid calldata");
// 限制可调用的函数白名单
bytes4 funcSig = bytes4(data);
require(isAllowedFunction(funcSig), "Function not allowed");
(bool success, ) = target.call(data);
require(success, "Call failed");
}
// 函数白名单验证
function isAllowedFunction(bytes4 funcSig) internal pure returns (bool) {
return funcSig == bytes4(keccak256("safeFunction()")) ||
funcSig == bytes4(keccak256("anotherSafeFunction(uint256)"));
}
五、总结
Call函数作为Solidity中的底层调用接口,虽然功能强大但存在严重安全隐患。开发者必须充分理解其工作原理和安全风险,避免直接暴露给不可信输入。通过案例分析可以看出,即使是标准库的组合使用也可能产生意想不到的漏洞。智能合约开发应遵循最小权限原则,实施深度防御策略,并在上线前进行充分的安全审计。