智能合约安全之合约检测绕过
字数 936 2025-08-22 12:23:24
智能合约安全:合约检测绕过与防御措施
文章前言
智能合约是区块链技术的重要应用之一,能够实现去中心化的交易和智能化的合约执行。然而智能合约安全问题一直是困扰区块链行业的难题。本文将详细介绍合约中对于合约地址检查的方法及其绕过方式,为合约安全审计人员和研发人员提供安全思考。
交互方式
在以太坊智能合约中,交互场景主要分为两种:
内部交互
合约与合约之间的交互。示例:
pragma solidity ^0.8.0;
contract ContractB {
uint256 public number;
function setNumber(uint256 _number) public {
number = _number;
}
function getNumber() public view returns (uint256) {
return number;
}
}
contract ContractA {
ContractB public contractB;
constructor(ContractB _contractB) {
contractB = _contractB;
}
function getNumberFromB() public view returns (uint256) {
return contractB.getNumber();
}
}
外部交互
合约与外部账号之间的交互。示例:
pragma solidity ^0.8.0;
contract ContractA {
function transferEther(address payable recipient) public payable {
recipient.transfer(msg.value);
}
}
交互账号
在以太坊智能合约中,交互的账号主要有两种:
合约账号
部署在以太坊网络上的智能合约,可以持有以太币和其他代币,拥有自己的存储空间和状态。示例:
pragma solidity ^0.8.0;
contract ContractA {
uint256 public count;
function increment() public payable {
count++;
}
}
外部账号
在以太坊网络上注册的账户,可以持有以太币和其他代币,但不具有存储空间和状态。示例:
pragma solidity ^0.8.0;
contract ContractA {
function transferEther(address payable recipient) public payable {
recipient.transfer(msg.value);
}
}
地址检查的必要性
在编写智能合约时对合约地址进行检测是为了防止合约调用和交互中的安全漏洞和错误:
- 避免调用不存在的合约:防止交易失败或资金损失
- 避免重入攻击安全风险:防止通过重复调用函数导致数据错误或资金损失
地址检查方法
使用extcodesize检查
pragma solidity ^0.8.0;
contract MyContract {
function transferTo(address payable _to) public payable {
require(isContract(_to), "Only contract address allowed");
_to.transfer(msg.value);
}
function isContract(address _addr) private view returns (bool) {
uint256 size;
assembly {
size := extcodesize(_addr)
}
return size > 0;
}
}
避免调用不存在的合约
pragma solidity ^0.8.0;
contract MyContract {
function callOtherContract(address _contractAddress) public returns (uint256) {
require(isContract(_contractAddress), "Contract address does not exist");
return OtherContract(_contractAddress).getValue();
}
function isContract(address _addr) private view returns (bool) {
uint256 size;
assembly {
size := extcodesize(_addr)
}
return size > 0;
}
}
contract OtherContract {
uint256 public value = 42;
function getValue() public view returns (uint256) {
return value;
}
}
避免重入攻击
pragma solidity ^0.8.0;
contract Bank {
mapping(address => uint256) balances;
bool private locked;
function withdraw(uint256 _amount) public {
require(_amount <= balances[msg.sender], "Insufficient balance.");
require(isContract(msg.sender) == false, "Contracts are not allowed to withdraw.");
require(payable(msg.sender).send(_amount), "Withdrawal failed.");
balances[msg.sender] -= _amount;
}
function isContract(address _addr) private view returns (bool) {
uint256 size;
assembly {
size := extcodesize(_addr)
}
return size > 0;
}
}
检测绕过方法
基本绕过方式
创建一个合约然后将其代码清空,使extcodesize返回0:
pragma solidity ^0.8.0;
contract Malicious {
address payable public owner = payable(msg.sender);
function() external payable {
owner.transfer(msg.value);
}
}
contract Test {
function isContract(address addr) public view returns (bool) {
uint256 codeSize;
assembly {
codeSize := extcodesize(addr)
}
return codeSize > 0;
}
function test(address payable addr) public payable {
require(isContract(addr), "Not a contract address");
(bool success,) = addr.call{value: msg.value}("");
require(success, "Call failed");
}
}
构造函数绕过
在构造函数中执行攻击代码,此时extcodesize仍为0:
pragma solidity ^0.8.0;
contract MaliciousContract {
address public bankAddress;
constructor(address _bankAddress) {
bankAddress = _bankAddress;
assembly {
mstore(0x00, 0x37) // 0x37 is the opcode of "extcodesize"
mstore(0x04, bankAddress) // call extcodesize with bankAddress as argument
// the result will be stored in memory at offset 0x00
let size := call(5000, 0x04, 0x00, 0, 0, 0, 0)
// if the result is non-zero, it means bankAddress is a contract
if gt(size, 0) {
// perform a reentry attack
// ...
}
}
}
}
防御方案
1. 使用OpenZeppelin的Address.sol(仍有缺陷)
pragma solidity ^0.4.18;
contract attack {
function attack(address _addr) public payable {
_addr.call.value(msg.value)();
}
}
2. 更佳方案:使用EIP-1052的extcodehash
pragma solidity 0.6.2;
library AddressUtils {
function isContract(address _addr) internal view returns (bool addressCheck) {
bytes32 codehash;
bytes32 accountHash = 0xc5d2460186f7233c927e7db2dcc703c0e500b653ca82273b7bfad8045d85a470;
assembly {
codehash := extcodehash(_addr)
}
addressCheck = (codehash != 0x0 && codehash != accountHash);
}
}
3. 其他防御措施
- 通过
tx.origin == msg.sender来校验调用者:确保调用者是外部账户而非合约 - 采用EIP-1502来进行检测:更安全的合约检测标准
总结
智能合约安全审计中,对合约地址的检测是防止多种攻击手段的重要防线。传统的extcodesize检查方法存在被绕过的风险,特别是在构造函数执行期间。安全开发者应当:
- 了解传统检测方法的局限性
- 采用更安全的
extcodehash方法(EIP-1052) - 结合多种防御手段,如
tx.origin检查 - 特别注意构造函数中的潜在攻击向量
通过全面理解这些检测方法和绕过技术,开发者可以构建更安全的智能合约系统。