以太坊Fuzz技术分析

以太坊Fuzz技术分析

一、前置知识

1. 智能合约的编译

1.1 合约的编译信息

在部署到以太坊网络之前,智能合约需要经过编译生成字节码(Bytecode)与abi,前者对应EVM的操作指令,后者提供调用的接口说明。

以一个简单的合约为例:1_Storage.sol,这是Remix编译器的默认合约之一。

// SPDX-License-Identifier: GPL-3.0

pragma solidity ^0.5.6;

contract Storage {

    uint256 number;

    function store(uint256 num) public {
        number = num;
    }

    function retrieve() public view returns (uint256){
        return number;
    }
}

经过在线编译后我们可以获得其对应的bytecode:

608060405234801561001057600080fd5b50610150806100206000396000f3fe608060405234801561001057600080fd5b50600436106100365760003560e01c80632e64cec11461003b5780636057361d14610059575b600080fd5b610043610075565b60405161005091906100a1565b60405180910390f35b610073600480360381019061006e91906100ed565b61007e565b005b60008054905090565b8060008190555050565b6000819050919050565b61009b81610088565b82525050565b60006020820190506100b66000830184610092565b92915050565b600080fd5b6100ca81610088565b81146100d557600080fd5b50565b6000813590506100e7816100c1565b92915050565b600060208284031215610103576101026100bc565b5b6000610111848285016100d8565b9150509291505056fea2646970667358221220522334dfd7decc643eeb644e28d6d7f11bae5f5b74d14e33980a35d12bc7771f64736f6c63430008110033

abi提供了所有函数信息,相当于接口文档:

[
    {
        "inputs": [],
        "name": "retrieve",
        "outputs": [
            {
                "internalType": "uint256",
                "name": "",
                "type": "uint256"
            }
        ],
        "stateMutability": "view",
        "type": "function"
    },
    {
        "inputs": [
            {
                "internalType": "uint256",
                "name": "num",
                "type": "uint256"
            }
        ],
        "name": "store",
        "outputs": [],
        "stateMutability": "nonpayable",
        "type": "function"
    }
]

对于开发者,abi与bytecode主要用于前端与链上合约的交互。如在web3.js中部署合约时,我们需要提供合约的abi与bytecode:

var rsContract=new web3.eth.Contract(abi).deploy({
    data:'0x'+bytecode,
    arguments:[],
}).send({
    from:data[0],
    gas:1500000,
    gasPrice:'30000000000000'
})

创建交易时,我们需要提供合约相应的abi与合约地址以初始化合约对象。

var MyContract = new web3.eth.Contract(abi,newContractAddress);

1.2 编译工具:solc

我们可以通过solc本地编译智能合约,得到相应的 bytecodeabi 以及 语法树 信息,语法树可以帮助我们进行数据流分析,但是在FUZZ方法中我们可以暂时先不考虑。

安装步骤

solc可以使用solc-select自动管理

  • 首先pip安装 solc-select:

    pip install solc-select
  • 安装选定的版本:

    solc-select install 0.5.6
  • 选择下载好的版本:

    solc-select use 0.5.6
  • 测试solc:

    >solc --version
    solc, the solidity compiler commandline interface
    Version: 0.5.6+commit.b259423e.Windows.msvc
编译合约
  • 生成abi信息:

    >solc --abi 1.sol
    
    ======= 1.sol:Storage =======
    Contract JSON ABI
    [{"constant":true,"inputs":[],"name":"retrieve","outputs":[{"name":"","type":"uint256"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":false,"inputs":[{"name":"num","type":"uint256"}],"name":"store","outputs":[],"payable":false,"stateMutability":"nonpayable","type":"function"}]
  • 生成bytecode信息:

    >solc --bin 1.sol
    
    ======= 1.sol:Storage =======
    Binary:
    608060405234801561001057600080fd5b5060bd8061001f6000396000f3fe6080604052348015600f57600080fd5b506004361060325760003560e01c80632e64cec11460375780636057361d146053575b600080fd5b603d607e565b6040518082815260200191505060405180910390f35b607c60048036036020811015606757600080fd5b81019080803590602001909291905050506087565b005b60008054905090565b806000819055505056fea165627a7a72305820570444b01ad23a2c48a0572597320a487e38faa715e16ab1f8aeb1ff76c449c90029
  • 生成AST信息:

    >solc --ast 1.sol
    Syntax trees:
    
    ======= 1.sol =======
    PragmaDirective
     Gas costs: 0
     Source: "pragma solidity ^0.5.6;"
    ContractDefinition "Storage"
     Source: "contract Storage {\r\n\r\n    uint256 number;\r\n\r\n\r\n    function store(uint256 num) public {\r\n        number = num;\r\n    }\r\n\r\n    function retrieve() public view returns (uint256){\r\n        return number;\r\n    }\r\n}"
    VariableDeclaration "number"
       Type: uint256
       Gas costs: 0
       Source: "uint256 number"
      ElementaryTypeName uint256
         Source: "uint256"
    FunctionDefinition "store" - public
       Source: "function store(uint256 num) public {\r\n        number = num;\r\n    }"
      ParameterList
         Gas costs: 0
         Source: "(uint256 num)"
        VariableDeclaration "num"
           Type: uint256
           Source: "uint256 num"
          ElementaryTypeName uint256
             Source: "uint256"
      ParameterList
         Gas costs: 0
         Source: ""
      Block
         Source: "{\r\n        number = num;\r\n    }"
        ExpressionStatement
           Gas costs: 20014
           Source: "number = num"
          Assignment using operator =
             Type: uint256
             Source: "number = num"
            Identifier number
               Type: uint256
               Source: "number"
            Identifier num
               Type: uint256
               Source: "num"
    FunctionDefinition "retrieve" - public - const
       Source: "function retrieve() public view returns (uint256){\r\n        return number;\r\n    }"
      ParameterList
         Gas costs: 0
         Source: "()"
      ParameterList
         Gas costs: 3
         Source: "(uint256)"
        VariableDeclaration ""
           Type: uint256
           Source: "uint256"
          ElementaryTypeName uint256
             Source: "uint256"
      Block
         Source: "{\r\n        return number;\r\n    }"
        Return
           Gas costs: 208
           Source: "return number"
          Identifier number
             Type: uint256
             Source: "number"

    我们在数据流分析中会使用 crytic_compile 的python第三方包对solc生成的语法树进一步封装,具体可看数据流分析章节

2. 以太坊字节码

以1_Storage.sol为例,其字节码翻译成EVM指令为:

PUSH1 0x80 PUSH1 0x40 MSTORE CALLVALUE DUP1 ISZERO PUSH2 0x10 JUMPI PUSH1 0x0 DUP1 REVERT JUMPDEST POP PUSH2 0x150 DUP1 PUSH2 0x20 PUSH1 0x0 CODECOPY PUSH1 0x0 RETURN INVALID PUSH1 0x80 PUSH1 0x40 MSTORE CALLVALUE DUP1 ISZERO PUSH2 0x10 JUMPI PUSH1 0x0 DUP1 REVERT JUMPDEST POP PUSH1 0x4 CALLDATASIZE LT PUSH2 0x36 JUMPI PUSH1 0x0 CALLDATALOAD PUSH1 0xE0 SHR DUP1 PUSH4 0x2E64CEC1 EQ PUSH2 0x3B JUMPI DUP1 PUSH4 0x6057361D EQ PUSH2 0x59 JUMPI JUMPDEST PUSH1 0x0 DUP1 REVERT JUMPDEST PUSH2 0x43 PUSH2 0x75 JUMP JUMPDEST PUSH1 0x40 MLOAD PUSH2 0x50 SWAP2 SWAP1 PUSH2 0xA1 JUMP JUMPDEST PUSH1 0x40 MLOAD DUP1 SWAP2 SUB SWAP1 RETURN JUMPDEST PUSH2 0x73 PUSH1 0x4 DUP1 CALLDATASIZE SUB DUP2 ADD SWAP1 PUSH2 0x6E SWAP2 SWAP1 PUSH2 0xED JUMP JUMPDEST PUSH2 0x7E JUMP JUMPDEST STOP JUMPDEST PUSH1 0x0 DUP1 SLOAD SWAP1 POP SWAP1 JUMP JUMPDEST DUP1 PUSH1 0x0 DUP2 SWAP1 SSTORE POP POP JUMP JUMPDEST PUSH1 0x0 DUP2 SWAP1 POP SWAP2 SWAP1 POP JUMP JUMPDEST PUSH2 0x9B DUP2 PUSH2 0x88 JUMP JUMPDEST DUP3 MSTORE POP POP JUMP JUMPDEST PUSH1 0x0 PUSH1 0x20 DUP3 ADD SWAP1 POP PUSH2 0xB6 PUSH1 0x0 DUP4 ADD DUP5 PUSH2 0x92 JUMP JUMPDEST SWAP3 SWAP2 POP POP JUMP JUMPDEST PUSH1 0x0 DUP1 REVERT JUMPDEST PUSH2 0xCA DUP2 PUSH2 0x88 JUMP JUMPDEST DUP2 EQ PUSH2 0xD5 JUMPI PUSH1 0x0 DUP1 REVERT JUMPDEST POP JUMP JUMPDEST PUSH1 0x0 DUP2 CALLDATALOAD SWAP1 POP PUSH2 0xE7 DUP2 PUSH2 0xC1 JUMP JUMPDEST SWAP3 SWAP2 POP POP JUMP JUMPDEST PUSH1 0x0 PUSH1 0x20 DUP3 DUP5 SUB SLT ISZERO PUSH2 0x103 JUMPI PUSH2 0x102 PUSH2 0xBC JUMP JUMPDEST JUMPDEST PUSH1 0x0 PUSH2 0x111 DUP5 DUP3 DUP6 ADD PUSH2 0xD8 JUMP JUMPDEST SWAP2 POP POP SWAP3 SWAP2 POP POP JUMP INVALID LOG2 PUSH5 0x6970667358 0x22 SLT KECCAK256 MSTORE 0x23 CALLVALUE 0xDF 0xD7 0xDE 0xCC PUSH5 0x3EEB644E28 0xD6 0xD7 CALL SHL 0xAE 0x5F JUMPDEST PUSH21 0xD14E33980A35D12BC7771F64736F6C634300081100 CALLER

EVM采用单字节指令集,即指令范围为:[00, FF],具体对应关系可见附表,不进行详述,我们主要考虑几个问题:

如何确定合约的入口

我们进行FUZZ时,必须要先确定测试合约的入口点,即测试的是哪个函数,我们在前端或合约内部都是通过 CALLDATA 实现的,即 address.call(abi.encodeWithSignature("function()", data)),CALLDATA为函数签名与传参的编码,长度补全为36字节,其中前4个字节为函数签名,即为keccak256("function()")的前四个字节,后32个字节为函数传入的参数。

如:abi.encodeWithSignature("store(uint256)", 10) 返回 0x6057361d000000000000000000000000000000000000000000000000000000000000000a,其中6057361dstore(uint256)的哈希,000000000000000000000000000000000000000000000000000000000000000a为传入的参数10。

我们关注上面bytecode中的这一段字节码:

60003560e01c80632e64cec11461003b5780636057361d1461005957

此处为函数选择器,翻译成操作码后为:

60 00                       =           PUSH1 0x00 
35                          =           CALLDATALOAD
60 e0                       =           PUSH1 0xe0
1c                          =           SHR
80                          =           DUP1  
63 2e64cec1                 =           PUSH4 0x2e64cec1
14                          =           EQ
61 003b                     =           PUSH2 0x003b
57                          =           JUMPI
80                          =           DUP1 
63 6057361d                 =           PUSH4 0x6057361d     
14                          =           EQ
61 0059                     =           PUSH2 0x0059
57                          =           JUMPI

我们可以看到有两个PUSH4操作,对应的是合约中的两个函数签名。该处完整流程:

  • 首先通过 CALLDATALOAD将 CALLDATA压入栈中
  • SHR右移e0位,即32个字节,栈上剩4字节即为函数签名
  • 通过EQ与两个PUSH4压入的合约中函数签名比较,若相等,通过JUMPI跳转到函数对应位置

因此我们发现:

  • bytecode中通过PUSH4、EQ与JUMPI联用作为函数选择器,同时包含函数签名信息,我们在进行测试参数生成时可以首先通过PUSH4的特征提取出所有函数签名
  • 函数的参数类型可以从abi中获取
  • 通过提供合法的函数签名与参数即可进入入口函数

EVM解析操作码逻辑

FUZZ最终检测漏洞时,需要提供整个操作流程的某些信息,如分析重入漏洞时,需要函数调用栈、操作码栈等信息,这些EVM并不关心也没有提供原型供直接调用,因此我们需要对每个对EVM环境产生影响的操作进行hook,具体来说,就是在每个操作码执行的前后插入我们的代码。

对于EVM解析操作码的流程进行简要分析,大致如下:

判别操作码 → 调用操作码函数 → 根据pc跳转下一操作码 → 遇到结束操作码 → 返回

具体代码可关注/core/vm/下文件:

evm.go
228         ret, err = evm.interpreter.Run(contract, input, false)

调用解释器运行字节码

interpreter.go
180 for {
187     op = contract.GetOp(pc)
188     operation := in.cfg.JumpTable[op]

238     res, err = operation.execute(&pc, in, callContext)
239     if err != nil {
240         break
241     }
242     pc++
243 }

245 if err == errStopToken {
246     err = nil // clear stop token error
247 }

循环获取字节码,从JumpTable中获取对应的操作码函数,进入执行流程,pc++,遇到结束操作码退出执行

instructions.go
29  func opAdd(pc *uint64, interpreter *EVMInterpreter, scope *ScopeContext) ([]byte, error) {
30      x, y := scope.Stack.pop(), scope.Stack.peek()
31      y.Add(&x, y)
32      return nil, nil
33  }

对应具体操作码函数,与栈和内存进行交互,此处没必要改动

因此,我们的主要hook代码可以插在evm.go和interpreter.go中,在处理操作码前后与函数调用前后更新我们维护的全局变量,或者可以直接封装一个解释器代理。

二、ContractFuzzer

我们通过ContractFuzzer来理解以太坊Fuzz的具体操作。ContractFuzzer是2018年被提出的一项智能合约Fuzz工具,可以在没有源码的条件下,基于智能合约的abi与bin文件进行多种漏洞的检测。作为该领域的先驱工具之一,其主体框架与思路目前仍受到大量工具借鉴与使用,具有较高的学习价值。经过笔者分析,该工具很好的封装了以太坊解释器,提供全局的函数调用栈、操作码栈、异常处理栈等供直接使用,前端代理合约采用巧妙的传参方式固定了重入合约的调用入口,主体主要分为3个部分:

  • Fuzzer:通过随机化输入参数,构造不同输入序列传递给私链进行合约函数的调用,将生成结果发给中继器,同时启动FuzzServer,监听合约调用结果信息
  • 中继器:将调用参数发送给代理合约,同时在私链上进行普通调用与代理调用
  • 私链:通过重写的geth部署私链,运行过程中根据EVM环境信息检测17种漏洞,将结果信息发送给FuzzServer

原论文链接:http://jiangbo.buaa.edu.cn/ContractFuzzerASE18.pdf,文中仅简单描述了原理与测试结果,对于我们理解Fuzz流程没有任何帮助,根据源文件的架构与具体代码,笔者总结了简化版脑图:https://www.processon.com/v/63a9a3846a25980941fdf627,文字分析如下,顺序参考文件架构。

1. base

该文件夹下仅提供工具依赖的dockerfile,无价值

2. examples

该文件夹提供了简单的测试合约,供使用者快速体验工具的整体流程,以delegatecall_dangerous为例,其下文件为:

delegatecall_dangerous
    |- fuzzer
        |- config
            |-addressSeed.1.json
            |-addressSeed.json
            |-addrmap.csv
            |-byteSeed.json
            |-bytesSeed.json
            |-contracts.list
            |-intSeed.json
            |-stringSeed.json
            |-uintSeed.json
        |- reporter
    |- verified_contract_abi_sigs
    |- verified_contract_abis
    |- verified_contract_bin_sigs
    |- verified_contract_bins
    |- verified_contract_configs
    |- verified_contract_sols
    |- delegatecall_dangerous.list

其中:

  • fuzzer文件夹提供fuzz的配置信息,*Seed.json对应不同参数的种子文件,contracts.list中记录需测试的合约名,addrmap.csv记录合约在私链上的地址

  • verified_contract_abis为合约的abi文件

  • verified_contract_abi_sigs为通过abi生成的函数签名,具体可见 tools 部分分析

  • verified_contract_bins为合约的bin文件,即字节码文件

  • verified_contract_bin_sigs为通过bin生成的函数签名对,具体可见 tools 部分分析

  • verified_contract_configs为测试合约的配置信息,部署后会自动在该配置信息中添加地址信息

  • verified_contract_sols为合约的源码

  • delegatecall_dangerous.list记录合约名,无实际作用

3. tools

该文件夹下为一些python编写的简单合约分析工具:

3.1 download_verified_contract_from_etherscan.py

从etherscan下载智能合约的abi与bin信息

3.2 excelUtil.py

封装了excel的读写操作

3.3 get_function_signature_from_abi.py

从abi中读取对应的函数与签名,加密函数:

def getFunId(Sig):
    # hashlib.s?
    s = sha3.keccak_256()
    s.update(Sig.encode("utf8"))
    hex = s.hexdigest()
    bytes4 = "0x"+hex[:8]
    return bytes4
    pass

由于abi为json文件,因此只需简单的反序列化操作即可

3.4 get_function_signature_pair_from_bin.py

从bin中读取对应的函数签名对,函数签名对即每个函数的签名与该函数调用的函数签名列表,具体步骤:

  • 使用 evm disasm 将字节码转换为操作码
  • readFunSigs函数通过PUSH4与EQ操作码提取所有函数选择器处函数签名与跳转位置
  • readSegs函数通过合并JUMP前后文合成一个函数全部字节码
  • read_innercall_sigs_from_codeseg函数通过PUSH4与CALL操作码确认函数体中调用的函数签名

4. Ethereum

该文件夹下为私链部署配置文件与持久化信息

4.1 gethrun.sh

私链启动命令:

geth --fast  --identity "TestNode2" --rpc -rpcaddr "0.0.0.0"  --rpcport "8545" --rpccorsdomain "*" --port "30303" --nodiscover  --rpcapi "db,eth,net,web3,miner,net,personal,net,txpool,admin"  --networkid 1900   --datadir /home/liuye/Ethereum --nat "any" --targetgaslimit "9000000000000"  --unlock 0 --password "pwd.txt"  --mine 

默认rpc端口为8545,链id为1900,此两项配置不建议修改,初始账户解锁密码从pwd.txt文件中获取

4.2 配置文件

  • keystore:初始五个测试账户数据

    UTC--2017-04-28T07-03-37.918061825Z--2b71cc952c8e3dfe97a696cf5c5b29f8a07de3d8
    UTC--2017-04-28T07-04-00.772291853Z--a31a0f4653f62aca35b6e986743c8f4fc6c8f38f
    UTC--2017-04-28T07-04-19.593238240Z--6d62f53305d3c247cd856a8a4eaf65518a7030cf
    UTC--2017-04-28T07-04-27.407171594Z--04c8862a82faf3fb90b73768c50dc7f23d7d26bd
    UTC--2017-04-28T07-04-45.312654824Z--271eab2a8058d1e3a45941f49d5671bb1cee8ca1
  • geth/transactions.rlp:交易日志

  • pwd.txt:解锁账户密码: 123456

    123456

5. go-ethereum-cf

该部分基于以太坊源码,修改了EVM部分代码,hook了操作码解析流程,主要改动集中在 core/vm 文件夹下

5.1 原文件修改:evm.go、interpreter.go

以太坊在发生交易时调用StateTransition.TransitionDb() 函数修改数据库中链上交易信息,其中调用evm.Call()函数调用合约,根据前置知识分析,主要修改evm.gointerpreter.go文件即可。

  • evm.Call():

    if caller != nil && !isRelOracle(contract.Address()) {
        Println("\nclose call...")
        /**
        *Call action finish. So pop the object on top of the stack.
        *and set object to "close" state, also record final related state.
        **/
        if hacker_call_stack != nil {
            // 函数调用栈弹出
            call := hacker_call_stack.pop()
            call.nextRevisionId = nextRevisionId
            if err != nil {
                call.throwException = true
                if strings.EqualFold(ErrOutOfGas.Error(), err.Error()) {
                    call.errOutGas = true
                }
            }
            if call == nil {
                Println("call is nil")
                return
            }
            call.OnCloseCall(*new(big.Int).SetUint64(contract.Gas))
            if hacker_call_stack.len() == 1 {
                hacker_close()
            }
        }
    }

    当每次函数调用结束后,弹出函数调用栈,如此时调用栈仅剩一项(初始EVM调用测试合约),进行漏洞检测

  • interpreter.Run()

        res, err := Hacker_record(op, (opFunc)(operation.execute), &pc, in.evm, contract, mem, stack)

    此处使用Hacker_record函数代理操作码解析过程,该函数在EVM状态改变前先修改相应的全局变量

5.2 hacker_contractcall.go

5.2.1 结构体:HackerContractCall
type HackerContractCall struct {
    isInitCall     bool
    caller         common.Address
    callee         common.Address
    value          big.Int
    gas            big.Int
    finalgas       big.Int
    input          []byte
    nextcalls      []*HackerContractCall
    OperationStack *HackerOperationStack
    StateStack     *HackerStateStack
    throwException bool
    errOutGas      bool
    errOutBalance  bool
    snapshotId     int
    nextRevisionId int
}

该结构体封装每个函数调用过程,主要属性如下:

  • isInitCall:是否为初始调用的测试合约函数

  • caller:调用合约地址

  • callee:被调用合约地址

  • value:函数传入的以太坊

  • gas:函数调用gas费

  • finalgas:最终消耗gas费

  • input:传入参数

  • nextcalls:后续调用函数

  • OperationStack:函数操作码栈

  • StateStack:状态栈

  • throwException:是否抛出异常

  • errOutGas:是否gas费不足

  • errOutBalance:是否余额不足

5.2.2 newHackerContractCall()

创建新的HackerContractCall,初始化操作码栈,第一项为传入的operartion;初始化状态栈为空;初始化nextcalls为空;初始化input参数

5.2.3 hacker_init()

在 evm.Call()函数中调用,如当前全局环境变量为空,则初始化函数列表、函数哈希列表、函数调用栈,压入测试合约函数作为函数调用栈第一项

    if hacker_env == nil || hacker_call_stack == nil {
        hacker_env = evm
        hacker_call_stack = newHackerContractCallStack()
        hacker_call_hashs = make([]common.Hash, 0, 0)
        hacker_calls = make([]*HackerContractCall, 0, 0)
        initCall := newHackerContractCall("STARTRECORD", contract.Caller(), contract.Address(), *contract.Value(), *new(big.Int).SetUint64(contract.Gas), contract.Input)
        initCall.isInitCall = true
        hacker_call_stack.push(initCall)
    }
5.2.4 hacker_close()

在 evm.Call()函数最后调用,即本次EVM执行流程结束执行

  • 清空函数调用栈

  • 创建所有漏洞测试机

        oracles := make([]Oracle, 0, 0)
        oracles = append(oracles, NewHackerReentrancy())
        ......
        oracles = append(oracles, NewHackerBalanceGtZero())
        features := make([]string, 0, 0)
        for _, oracle := range oracles {
            oracle.InitOracle(hacker_call_hashs, hacker_calls)
            if true == oracle.TestOracle() {
                features = append(features, oracle.String())
            }
        }
  • 将测试结果发送给fuzzServer: http://localhost:8888/hack

        features_str, _ := json.Marshal(features)
        values := url.Values{"oracles": {string(features_str)}, "profile": {GetReportor().Profile(hacker_call_hashs, hacker_calls)}}
        url := "http://localhost:8888/hack?" + values.Encode()
5.2.5 onCall/onDelegateCall/onCallCode()

在evm.Call()/DelegateCall()/CallCode() 中调用,创建新的HackerContractCall,并压入函数调用栈

5.2.6 onOpcode()

除 Call/DelegateCall/CallCode 外的所有操作码,均仅执行两步操作:向操作码栈中压入本操作码,向状态栈中压入调用者与被调用者的状态体

    call.OperationStack.push(opCodeToString[RETURN])
    call.StateStack.push(newHackerState(call.caller, call.callee))

5.3 hacker_instruction.go

5.3.1 Hacker_record()

代理操作码处理逻辑,主要功能在执行原有操作码功能前,先调用Hacker_contractcall的onOpcode()函数

func Hacker_record(op OpCode, fun opFunc, pc *uint64, evm *EVM, contract *Contract, memory *Memory, stack *Stack) ([]byte, error) {
    if hacker_call_stack != nil {
        call := hacker_call_stack.peek()
        if call != nil {
            switch op {
            case DIV:
                call.OnDiv()
                ...
            case RETURN:
                call.OnReturn()
            default:
                break
            }
        }
    }
    // operation.execute(&pc, in.evm, contract, mem, stack)
    return fun(pc, evm, contract, memory, stack)
}

5.4 hacker_operation.go

5.4.1 结构体:HackerOperationStack

封装操作码栈,记录测试过程中每个操作码的调用顺序

type HackerOperationStack struct {
    data []string
}

5.5. hacker_state.go

主要封装函数调用过程中各合约状态

5.5.1 结构体:Hacker_ContractState
type Hacker_ContractState struct {
    addr    common.Address
    storage map[common.Hash]common.Hash
    balance big.Int
}

封装合约的地址、存储信息、余额信息

5.5.2 结构体:HackerState
type HackerState struct {
    contracts []*Hacker_ContractState
}

封装每次函数调用时调用者与被调用者的状态对

5.5.3 newHackerState()

创建HackerState,传入调用者与被调用者地址,封装成Hacker_ContractState后装入HackerState

func newHackerState(addrs ...common.Address) *HackerState {
    size := len(addrs)
    _contracts := make([]*Hacker_ContractState, 0, size)
    for _, addr := range addrs {
        _contracts = append(_contracts, newHacker_ContractState(addr))
    }
    return &HackerState{contracts: _contracts}
}
5.5.4 结构体:HackerStateStack
type HackerStateStack struct {
    data []*HackerState
}

封装整个测试过程中调状态用对栈

5.6 hacker_utils.go

主要封装一些工具类函数

5.6.1 ShowDiffState()

判断两个合约地址是否一致,包括balance与storage的差异

5.6.2 Hash()

对一次函数调用进行哈希,哈希值=Hash(caller)+Hash(callee)+Hash(input)

5.7 hacker_hub.go

isRelOracle()

判断地址是否与0xfa7b9770ca4cb04296cac84f37736d4041251cdf一致,该地址为以太坊启动时创建的更新版本合约地址,在geth.makeFullNode()中被初始化。该函数在evm.Call()等处被调用,用于判断此函数调用是否来自以太坊更新合约,若是,则不进行hook。

var relOracle = common.HexToAddress("0xfa7b9770ca4cb04296cac84f37736d4041251cdf")

func isRelOracle(addr common.Address) bool {
    if addr.Big().Cmp(relOracle.Big()) == 0 {
        return true
    } else {
        return false
    }
}

5.8 hacker_oracle.go

提供17种漏洞测试机,对于不同条件检测并生成测试报告

5.8.1 接口:Oracle
type Oracle interface {
    InitOracle(hacker_call_hashs []common.Hash, hacker_calls []*HackerContractCall)
    TestOracle() bool
    Write(writer io.Writer)
    String() string
}

提供测试机的基本接口函数,其中InitOracle()与TestOracle()都在hacker_close()函数中调用

5.8.2 根调用失败:HackerRootCallFailed
type HackerRootCallFailed struct {
    hacker_call_hashs []common.Hash
    hacker_calls      []*HackerContractCall
}
  • hacker_call_hashs:函数哈希列表

  • hacker_calls:函数列表

  • TestOracle():判断跟调用是否抛出异常

    func (oracle *HackerRootCallFailed) TestOracle() bool {
    var rootCall = oracle.hacker_calls[0]
    return rootCall.throwException
    }
5.8.3 重入漏洞:HackerReentrancy
type HackerReentrancy struct {
    hacker_call_hashs []common.Hash
    hacker_calls      []*HackerContractCall
    repeatedPairs     [][2]*HackerContractCall
    feauture          string
}
  • repeatedPairs:重复函数对

  • feauture:漏洞描述

  • TestOracle():判断函数调用列表中是否存在某项与第一项一致,且其操作栈大小不小于第一项,如果操作栈小于第一项,认为有条件判断,无法完成重入操作,此处判断并不完善

    func (oracle *HackerReentrancy) TestOracle() bool {
    var hasReen bool
    hasReen = false
    i := 0
    hash1 := oracle.hacker_call_hashs[i]
    for j := i + 1; j < len(oracle.hacker_call_hashs); j++ {
        hash2 := oracle.hacker_call_hashs[j]
        if strings.Compare(hash1.String(), hash2.String()) == 0 {
            if oracle.hacker_calls[i].OperationStack.len() <= oracle.hacker_calls[j].OperationStack.len() {
                oracle.feauture = "le"
            } else {
                oracle.feauture = "anti-reentrancy"
            }
            repeatedPair := [2]*HackerContractCall{oracle.hacker_calls[i], oracle.hacker_calls[j]}
            oracle.repeatedPairs = append(oracle.repeatedPairs, repeatedPair)
            hasReen = true
        }
    }
    return hasReen
    }
5.8.4 重复函数调用:HackerRepeatedCall
type HackerRepeatedCall struct {
    hacker_call_hashs []common.Hash
    hacker_calls      []*HackerContractCall
    repeatedPairs     [][2]*HackerContractCall
}
  • TestOracle():判断是否存在两个重复的函数调用

    func (oracle *HackerRepeatedCall) TestOracle() bool {
    hasRepeated := false
    //nextcalls_len := len(oracle.hacker_calls[0].nextcalls)
    for i, _ := range oracle.hacker_call_hashs {
        hash1 := oracle.hacker_call_hashs[i]
        for j := i + 1; j < len(oracle.hacker_call_hashs); j++ {
            hash2 := oracle.hacker_call_hashs[j]
            if strings.Compare(hash1.String(), hash2.String()) == 0 &&
                oracle.hacker_calls[i].isBrother(i, oracle.hacker_calls[j]) {
                repeatedPair := [2]*HackerContractCall{oracle.hacker_calls[i], oracle.hacker_calls[j]}
                oracle.repeatedPairs = append(oracle.repeatedPairs, repeatedPair)
                hasRepeated = true
            }
        }
    }
    return hasRepeated
    }
5.8.5 存在以太坊传递的函数调用:HackerEtherTransfer
type HackerEtherTransfer struct {
    hacker_call_hashs      []common.Hash
    hacker_calls           []*HackerContractCall
    hacker_exception_calls []*HackerContractCall
}
  • hacker_exception_calls:满足条件的函数列表

  • TestOracle():判断函数调用的value参数是否大于0,不包括初始调用测试函数

    func (oracle *HackerEtherTransfer) TestOracle() bool {
    ret := false
    calls := oracle.hacker_calls[0].nextcalls
    for _, call := range calls {
        if oracle.triggerOracle(call) {
            oracle.hacker_exception_calls = append(oracle.hacker_exception_calls, call)
            ret = true
        }
    }
    return ret
    }
    
    func (oracle *HackerEtherTransfer) triggerOracle(call *HackerContractCall) bool {
    nextcalls := call.nextcalls
    for _, n_call := range nextcalls {
        if n_call.value.Uint64() > 0 {
            return true
        }
    }
    return call.value.Uint64() > 0
    }
5.8.6 以太坊传递失败的函数调用:HackerEtherTransferFailed
type HackerEtherTransferFailed struct {
    hacker_call_hashs      []common.Hash
    hacker_calls           []*HackerContractCall
    hacker_exception_calls []*HackerContractCall
}
  • TestOracle():判断函数调用是否存在以太坊传递或调用BALANCE操作码且抛出异常

    func (oracle *HackerEtherTransferFailed) TestOracle() bool {
    ret := false
    if oracle.triggerOracle(oracle.hacker_calls[0]) {
        oracle.hacker_exception_calls = append(oracle.hacker_exception_calls, oracle.hacker_calls[0])
        ret = true
    }
    calls := oracle.hacker_calls[0].nextcalls
    for _, call := range calls {
        if oracle.triggerOracle(call) {
            oracle.hacker_exception_calls = append(oracle.hacker_exception_calls, call)
            ret = true
        }
    }
    return ret
    }
    func (oracle *HackerEtherTransferFailed) triggerOracle(call *HackerContractCall) bool {
    
    return (call.value.Uint64() > 0 || strings.Contains(call.OperationStack.String(), opCodeToString[BALANCE])) && call.throwException
    }
5.8.7 传递以太坊的CALL函数调用:HackerCallEtherTransferFailed
type HackerCallEtherTransferFailed struct {
    hacker_call_hashs     []common.Hash
    hacker_calls          []*HackerContractCall
    hacker_fallback_calls []*HackerContractCall
}
  • TestOracle():

    • 被调用者为账户地址
    • gas费大于2300,因为send调用的函数默认为2300gas费
    • 函数抛出异常
    • 函数传递以太坊>0
    func (oracle *HackerCallEtherTransferFailed) TestOracle() bool {
    var hasCallEtherTransferFailed bool = false
    calls := oracle.hacker_calls[0].nextcalls
    for _, call := range calls {
        if true == oracle.TriggerFallbackCall(call) {
            oracle.hacker_fallback_calls = append(oracle.hacker_fallback_calls, call)
            hasCallEtherTransferFailed = true
        }
    }
    return hasCallEtherTransferFailed
    }
    func (oracle *HackerCallEtherTransferFailed) TriggerFallbackCall(call *HackerContractCall) bool {
    return IsAccountAddress(call.callee) && call.gas.Uint64() > 2300 && call.throwException && call.value.Uint64() > 0
    }
5.8.8 gas费不足的SEND函数调用
type HackerGaslessSend struct {
    hacker_call_hashs      []common.Hash
    hacker_calls           []*HackerContractCall
    hacker_exception_calls []*HackerContractCall
}
  • TestOracle():

    • 被调用者为账户地址
    • gas费等于2300,因为send调用的函数默认为2300gas费
    • 函数抛出gas费不足异常
    • 函数传参为空
    func (oracle *HackerGaslessSend) TestOracle() bool {
    hasException := false
    calls := oracle.hacker_calls[0].nextcalls
    for _, call := range calls {
        if oracle.TriggerExceptionCall(call) {
            oracle.hacker_exception_calls = append(oracle.hacker_exception_calls, call)
            hasException = true
        }
    }
    return hasException
    }
    func (oracle *HackerGaslessSend) TriggerExceptionCall(call *HackerContractCall) bool {
    return IsAccountAddress(call.callee) && call.throwException == true && call.errOutGas == true && len(call.input) == 0 && call.gas.Uint64() == 2300
    }
5.8.9 DELEGATE函数调用:HackerDelegateCallInfo
type HackerDelegateCallInfo struct {
    hacker_call_hashs     []common.Hash
    hacker_calls          []*HackerContractCall
    hacker_delegate_calls []*HackerContractCall
    feautures             []string
}
  • TestOracle():判断操作码调用栈中是否有 DELEGATECALL

    func (oracle *HackerDelegateCallInfo) TestOracle() bool {
    var hasDelegate bool
    hasDelegate = false
    nextcalls := oracle.hacker_calls[0].nextcalls
    for _, call := range nextcalls {
        if oracle.TriggerDelegateCall(call) {
            oracle.hacker_delegate_calls = append(oracle.hacker_delegate_calls, call)
            hasDelegate = true
            oracle.GetFeatures(oracle.hacker_calls[0], call)
        }
    }
    return hasDelegate
    }
    func (oracle *HackerDelegateCallInfo) TriggerDelegateCall(call *HackerContractCall) bool {
    return strings.Contains(call.OperationStack.String(), opCodeToString[DELEGATECALL])
    }
5.8.10 异常无序:HackerExceptionDisorder

异常无序指多个函数调用中,底层函数抛出的异常未被顶层函数捕获

type HackerExceptionDisorder struct {
    hacker_call_hashs      []common.Hash
    hacker_calls           []*HackerContractCall
    hacker_exception_calls []*HackerContractCall
}
  • TestOracle():判断是否下层函数调用抛出异常而根函数调用未抛出异常

    func (oracle *HackerExceptionDisorder) TestOracle() bool {
    exception := false
    nextcalls := oracle.hacker_calls[0].nextcalls
    for _, call := range nextcalls {
        if oracle.TriggerExceptionCall(hacker_calls[0], call) {
            oracle.hacker_exception_calls = append(oracle.hacker_exception_calls, call)
            exception = true
        }
    }
    return exception
    }
    func (oracle *HackerExceptionDisorder) TriggerExceptionCall(root, call *HackerContractCall) bool {
    return root.throwException == false && IsAccountAddress(call.callee) && call.throwException == true
    }
5.8.11 SEND函数调用:HackerSendOpInfo
type HackerSendOpInfo struct {
    hacker_call_hashs      []common.Hash
    hacker_calls           []*HackerContractCall
    hacker_exception_calls []*HackerContractCall
}
  • TestOracle():判断函数gas费是否为2300且传参为空

    func (oracle *HackerSendOpInfo) TestOracle() bool {
    ret := false
    nextcalls := oracle.hacker_calls[0].nextcalls
    for _, call := range nextcalls {
        if oracle.triggerOracle(call) {
            oracle.hacker_exception_calls = append(oracle.hacker_exception_calls, call)
            ret = true
        }
    }
    return ret
    }
    func (oracle *HackerSendOpInfo) triggerOracle(call *HackerContractCall) bool {
    return IsAccountAddress(call.callee) && len(call.input) == 0 && call.gas.Uint64() == 2300
    }
5.8.12 CALL函数调用:HackerCallOpInfo
type HackerCallOpInfo struct {
    hacker_call_hashs      []common.Hash
    hacker_calls           []*HackerContractCall
    hacker_exception_calls []*HackerContractCall
}
  • TestOracle():判断函数gas费是否大于2300

    func (oracle *HackerCallOpInfo) TestOracle() bool {
    ret := false
    nextcalls := oracle.hacker_calls[0].nextcalls
    for _, call := range nextcalls {
        if oracle.triggerOracle(call) {
            oracle.hacker_exception_calls = append(oracle.hacker_exception_calls, call)
            ret = true
        }
    }
    return ret
    }
    func (oracle *HackerCallOpInfo) triggerOracle(call *HackerContractCall) bool {
    return IsAccountAddress(call.callee) && call.gas.Uint64() > 2300
    }
5.8.13 抛出异常的CALL函数调用:HackerCallExecption
type HackerCallExecption struct {
    hacker_call_hashs      []common.Hash
    hacker_calls           []*HackerContractCall
    hacker_exception_calls []*HackerContractCall
}
  • TestOracle():判断函数gas费是否大于2300且抛出异常

    func (oracle *HackerCallExecption) TestOracle() bool {
    ret := false
    nextcalls := oracle.hacker_calls[0].nextcalls
    for _, call := range nextcalls {
        if oracle.triggerOracle(call) {
            oracle.hacker_exception_calls = append(oracle.hacker_exception_calls, call)
            ret = true
        }
    }
    return ret
    }
    func (oracle *HackerCallExecption) triggerOracle(call *HackerContractCall) bool {
    return IsAccountAddress(call.callee) && call.throwException == true && call.gas.Uint64() > 2300
    }
5.8.14 函数调用可控:HackerUnknownCall

函数调用可控指函数调用中被调用者地址或函数传参为用户可控

type HackerUnknownCall struct {
    hacker_call_hashs      []common.Hash
    hacker_calls           []*HackerContractCall
    hacker_exception_calls []*HackerContractCall
}
  • TestOracle():

    • gas费>2300
    • 根调用的调用者与某项函数的被调用者一致
    • 或函数传参可在根函数调用传参中找到
    • 或函数被调用者地址可在根函数调用传参中找到
    func (oracle *HackerUnknownCall) TestOracle() bool {
    ret := false
    nextcalls := oracle.hacker_calls[0].nextcalls
    for _, call := range nextcalls {
        if oracle.TriggerOracle(oracle.hacker_calls[0], call) {
            oracle.hacker_exception_calls = append(oracle.hacker_exception_calls, call)
            ret = true
        }
    }
    return ret
    }
    func (oracle *HackerUnknownCall) TriggerOracle(rootCall, call *HackerContractCall) bool {
    var input_str = string(rootCall.input)
    var callee_str = strings.ToLower(call.callee.Hex()[2:])
    // EqualFold: 忽略大小写
    return call.gas.Uint64() > 2300 && (strings.EqualFold(strings.ToLower(rootCall.caller.Hex()), strings.ToLower(call.callee.Hex())) || strings.Contains(input_str, string(call.input)) || strings.Contains(input_str, callee_str))
    }
5.8.15 储存状态修改:HackerStorageChanged
type HackerStorageChanged struct {
    hacker_call_hashs      []common.Hash
    hacker_calls           []*HackerContractCall
    hacker_exception_calls []*HackerContractCall
}
  • TestOracle():判断根函数调用结束时调用者与被调用者的储存状态是否发生变化

    func (oracle *HackerStorageChanged) TestOracle() bool {
    ret := false
    if oracle.TriggerOracle(oracle.hacker_calls[0]) {
        oracle.hacker_exception_calls = append(oracle.hacker_exception_calls, oracle.hacker_calls[0])
        ret = true
    }
    return ret
    }
    func (oracle *HackerStorageChanged) TriggerOracle(rootCall *HackerContractCall) bool {
    rootStorage := rootCall.StateStack
    ret, _ := rootStorage.Data()[0].Cmp(rootStorage.Data()[rootStorage.len()-1])
    return ret != 0
    }
5.8.16 时间戳依赖:HackerTimestampOp
type HackerTimestampOp struct {
    hacker_call_hashs      []common.Hash
    hacker_calls           []*HackerContractCall
    hacker_exception_calls []*HackerContractCall
}
  • TestOracle():判断操作码栈中是否存在 TIMESTAMP:

    func (oracle *HackerTimestampOp) TestOracle() bool {
    var rootCall = hacker_calls[0]
    return strings.Contains(rootCall.OperationStack.String(), opCodeToString[TIMESTAMP])
    }
5.8.17 区块编号依赖:HackerNumberOp
type HackerNumberOp struct {
    hacker_call_hashs      []common.Hash
    hacker_calls           []*HackerContractCall
    hacker_exception_calls []*HackerContractCall
}
  • TestOracle():判断操作码栈中是否存在 NUMBER:

    func (oracle *HackerNumberOp) TestOracle() bool {
    var rootCall = hacker_calls[0]
    return strings.Contains(rootCall.OperationStack.String(), opCodeToString[NUMBER])
    }
5.8.18 区块哈希依赖:HackerBlockHashOp
type HackerBlockHashOp struct {
    hacker_call_hashs      []common.Hash
    hacker_calls           []*HackerContractCall
    hacker_exception_calls []*HackerContractCall
}
  • TestOracle():判断操作码栈中是否存在 TIMESTAMP:

    func (oracle *HackerBlockHashOp) TestOracle() bool {
    var rootCall = hacker_calls[0]
    return strings.Contains(rootCall.OperationStack.String(), opCodeToString[BLOCKHASH])
    }
5.8.19 完整调用信息:HackerCallInfoReportor
type HackerCallInfoReportor struct {
    hacker_call_hashs []common.Hash
    hacker_calls      []*HackerContractCall
    callsLen          int
    root              *HackerContractCall
    operationLen      int
    operationStack    *HackerOperationStack
}

主要包括:

  • hacker_call_hashs:完整调用中所有函数哈希
  • hacker_calls:完整调用中所有函数
  • callsLen:函数调用长度
  • root:根函数调用
  • operationLen:操作码栈长度
  • operationStack:操作码栈

通过Profile()生成结果,在hacker_close()中被调用,作为完整调用信息发送给fuzzServer

6. contract_fuzzer

该模块主要通过合约的abi生成fuzz测试序列,主题文件在 src/ContractFuzzer 文件夹下

6.1 contractfuzzer

入口文件:main.go

对abi_dir下的文件进行fuzz:

go fuzz.Start(*abi_dir,*out_dir)

开启fuzzServer:

go server.Start(*addr_map,*reporter)

6.2 abi

封装abi文件的序列化与反序列化等操作

6.3 config

不同type的fuzz种子序列,如地址的种子列表:

{"name":"AddressSeeds","seeds":["0xe930e50b62af818dbc955f345f9a3a3108f7a70d"],"seeds1":["0xe930e50b62af818dbc955f345f9a3a3108f7a70d"],"seeds2":["0x952fa21849f5e6ce0c3a233f6036caabb9e944e2","0xd9e2a443db97d545619ee4afa82535e506ed0913","0xc77c361688d6a81fd2b7a003ac599412ad9da854",......]}

其中0xe930e50b62af818dbc955f345f9a3a3108f7a70d为代理合约地址,可供重入漏洞测试

6.4 fuzz

主体函数:abi.fuzz()
  • 从所有函数列表中,生成随机数选取入口函数:

    197   for func_chose, err = g_func_Robin.Random_select(funcs); err == nil && func_chose.(*Function).Type != "function"; func_chose, err = g_func_Robin.Random_select(funcs) {
    198
    199   }
  • 根据输入参数的不同类型fuzz参数:

    207       if ret, err := f.Inputs.fuzz(); err == nil {
    208           return fmt.Sprintf("%s:[%s]", f.Sig(), ret.(string)), nil
    209       } else {
    210           return "0x0", err
    211       }

    如uint256与address的输入参数类型:

    [{"name":"_addr","type":"uint256"},{"name":"_value","type":"address"}]

    生成fuzz参数为:

    [{"name":"_addr","type":"uint256","out":["0x332d0a7a12412f0b2c9d51c6"]},{"name":"_value","type":"address","out":["0xe930e50b62af818dbc955f345f9a3a3108f7a70d"]}]
发送fuzz结果:sendMsg2RunnerMonitor()

address传入测试合约地址,msg传入fuzz结果,发送给runnerMonitor即中继器:

215 values := url.Values{"address":[]string{address},"msg":msgs}
216 go func(){
217         res,_ := Client.Get(Global_tester_port+"/runnerMonitor?"+values.Encode())

6.5 server

启动fuzzServer,默认监听地址为8888,接收合约执行结束后的测试结果,处理器为hackHandler()函数

633 http.HandleFunc("/hack", hackHandler)
634 log.Fatal(http.ListenAndServe(fuzz.Global_listen_port, nil))

7. contract_tester

该模块负责中转fuzz客户端与私链的通信,插入代理合约的调用

7.1 命令行:run.sh

启动命令:

#!/bin/sh
bnode ./utils/runFuzzMonitor --ip http://127.0.0.1:8545 --account 0 --value 0

ip参数为私链rpc地址,默认端口为8545

7.2 utils/runFuzzMonitor.js

部署代理合约函数:getAgent()

首先通过abi与bin先将代理合约部署在私链上:

async function getAgent(){
    let name = "Agent";
    let address = "0xe930e50b62af818dbc955f345f9a3a3108f7a70d";
    let abi = JSON.parse('[{"constant":true,"inputs":[],...{"payable":true,"type":"fallback"}]');
    let code = "6060...9056";
    let MyContract = truffle_Contract({
        contract_name: name,
        abi: abi,
        unlinked_binary: code,
        network_id: 1900,
        address: address,
        default_network: 1900
    });
    MyContract.setProvider(Provider);
    let Caller3 = await MyContract.deployed();
    return Caller3;
}

代理合约源码可在Agent/Agent.sol中找到,部分函数名有区别,整体逻辑一致:

contract Agent{
    uint public count = 0;
    address  public call_contract_addr;
    bytes  public  call_msg_data;
    bool public turnoff = true;
    bool public hasValue = false;
    uint public sendCount = 0;
    uint public sendFailedCount =0;
    function() payable{
     if (turnoff){
        count ++;
        //turnoff set to false before statement "call_contract_addr.call.." rather than afer.
        //As we aim to test reentrancy only one times and  
        //more times for reentrancy is unnecessary.
        turnoff = false;
        call_contract_addr.call(call_msg_data);

     }else{
        turnoff = true;
     }
    }
    function Agent(){

    }
    function getContractAddr() returns(address addr){
        return call_contract_addr;
    }
    function getCallMsgData() returns(bytes msg_data){
        return call_msg_data;
    }
    function AgentCallWithoutValue(address contract_addr,bytes msg_data){
        hasValue = false;
        call_contract_addr  = contract_addr;
        call_msg_data = msg_data;
        contract_addr.call(msg_data);
    }
    function AgentCallWithValue(address contract_addr,bytes msg_data) payable{
      hasValue = true;
      uint msg_value = msg.value;
      call_contract_addr  = contract_addr;
      call_msg_data = msg_data;
      contract_addr.call.value(msg_value)(msg_data);
    }
    function AgentSend(address contract_addr) payable{
        sendCount ++;
        if (!contract_addr.send(msg.value))
            sendFailedCount++;
    }
}
  • AgentCallWithoutValue()/AgentCallWithValue():

    通过传入测试合约地址与初始传参,保存在合约变量中,供fallback函数直接调用

  • function():

    使用call_contract_addr.call(call_msg_data);重新模拟初始调用,即重入函数

启动函数:RunnerMonitor()

默认监听8088端口,处理参数函数为go():

function RunnerMonitor(){
    const port = 8088;
    const http = require('http');
    const url = require('url');
    try{
        http.createServer(function (request, response) {
            console.log(request.url);
            let obj = url.parse(request.url,true);
            console.log(obj.query);
            let address = obj.query["address"];
            let msg_group = obj.query["msg"];

            if (address!=undefined || msg_group!=undefined){
                if (!(msg_group instanceof Array))
                    msg_group = [msg_group]
                go(address,msg_group);
            }
            response.writeHead(200, {'Content-Type': 'text/plain'});
            // 发送响应数据 "Hello World"
            response.end('Hello World\n');
        }).listen(port);
    }catch(err){
        console.log(err);
    }
    // 终端打印如下信息
    console.log('Monitor running at http://127.0.0.1:8088/');
}
调用合约函数:go()

发送三次交易:

function go(address,msg_group){
    let argsAgent = [];
    let args = [];
    let value = VALUE;
    for (let index=0;index<msg_group.length;index++){
        Robin_no++;
        value = Robin[Robin_no%Robin_Index];
        argsAgent.push({
            Caller: Agent,
            contract_addr: address,
            msg_data: msg_group[index],
            value:value
        });
        value = Robin[(Robin_no%Robin_Index+1)%Robin_Index];
        args.push({
            from: Owner,
            to: address,
            value: value,
            gas: defaultGas,
            data:  msg_group[index]
        });
        value = Robin[(Robin_no%Robin_Index+1)%Robin_Index];
        args.push({
            from: Normal,
            to: address,
            value: value,
            gas: defaultGas,
            data:  msg_group[index]
        });
    }
    MyCallWithValueBatch(argsAgent);
    sendBatchTransaction(args);
}
  • 第一次argsAgent合约地址为代理合约,参数为测试合约地址与传参,即调用了代理合约的AgentCallWithValue()函数初始化合约变量,供重入测试
  • 后两次都为对测试合约的函数调用,区别为发起者地址不同

8. contract_deployer

该模块负责将测试合约部署在私链上

8.1 命令行:deploy.sh

#!/bin/bash
set -e

workplace="$PWD"
echo "$workplace"
CALLERS="$workplace/caller"
BINS="$workplace/caller.bin"
ABIS="$workplace/caller.abi"
bnode ./utilsScripts/deployContract.js --contractdir $CONTRACTS -bindir  $BINS  --abidir $ABIS
#echo "$CALLERS"
# CONTRACTS="$workplace/contracts"
# BINS="$workplace/bin"
# ABIS="$workplace/abi"
# echo "$CONTRACTS"
# echo "$BINS"
# echo "$ABIS"
# bnode ./.js/scripts/deployContract.js --contractdir $CONTRACTS -bindir  $BINS  --abidir $ABIS

根据合约的abi与bin部署合约

8.2 配置文件:.env

主要配置私链rpc地址:默认端口为8545

#set your contract deploy config files path 
CONFIG_PATH=/ContractFuzzer/contract_deployer/examples/config
ABI_SUB_DIR=verified_contract_abis
BIN_SUB_DIR=verified_contract_bins
ABI_SUFFIX=.abi
BIN_SUFFIX=.bin

#Ethereum client rpc port. 
#Geth is popular client and this is also the client we do our fuzzer initially currently.
#So please feel relaxed that we just provide this geth option at present.
GethHttpRpcAddr = http://localhost:8545

三、附表:EVM操作码

OPCODE NAME 出栈 压栈 描述<div style="length=100px"> 注释<div style="length=100px">
00 STOP 停止操作 暂停执行并退出当前上下文
01 ADD a b a+b 加法操作
02 MUL a b a*b 乘法操作
03 SUB a b a-b 减法操作
04 DIV a b a/b 除法操作 无符号整除
05 SDIV a b a/b 除法操作 有符号整除,如果b=0,则结果也为0
06 MOD a b a%b 取余操作 无符号取余
07 SMOD a b a%b 取余操作 有符号取余
08 ADDMOD a b N (a+b)%N 累加取余 如果N=0,则结果也为0
09 MULMOD a b N (a*b)%N 累乘取余 如果N=0,则结果也为0
0A EXP a exponent a**exponent 取幂
0B SIGNEXTENT x b y 符号扩展 把b当作(x+1)个字节的有符号整数,将其扩展到32个字节
10 LT a b a&lt;b 小于 结果为 1/0
11 GT a b a>b 大于
12 SLT a b a&lt;b 有符号小于
13 SGT a b a>b 有符号大于
14 EQ a b a == b 等于
15 ISZERO a a==0 判断是否为零
16 AND a b a&b 按位与
17 OR a b a|b 按位或
18 XOR a b a^b 按位异或
19 NOT a ~a 按位取反
1A BYTE i x y 截取 取x的从高往低数第i+1个字节,x∈[0,31]
1B SHL shift value value &lt;&lt; shift 左移 value左移shift位
1C SHR shift value value >> shift 右移
1D SAR shift value value >> shift 有符号右移动 符号位固定,算数右移
20 SHA3 offset size hash Keccak-256哈希
30 ADDRESS address 获取当前执行账户地址
31 BALANCE address balance 获取给定地址余额
32 ORIGIN address 获取交易初始账户地址 tx.origin
33 CALLER address 获取调用者地址 msg.sender
34 CALLVALUE value 获取交易传入以太坊数
35 CALLDATALOAD i data[i] 获取当前环境传入参数 获取参数从高往低32-i个字节
36 CALLDATASIZE size 获取传入参数长度 参数字节数
37 CALLDATACOPY destOffset offset size 拷贝传入参数进内存 超出传入参数范围的复制0
38 CODESIZE size 获取当前环境代码长度
39 CODEOCPY destOffset offset size 拷贝当前环境代码进内存 超出代码范围的复制0
3A GASPRICE price 当前的gas费用
3B EXTCODESIZE address suze 获取地址的代码长度
3C EXTCODECOPY address destOffset offset size 拷贝地址代码进内存 超出代码范围的复制0
3D RETURNDATASIZE size 获取上次调用的返回参数长度
3E RETURNDATACOPY destOffset offset size 拷贝上次调用返回参数进内存
3F EXTCODEHASH address hash 获取地址的代码哈希
40 BLCKHASH blockNumber hash 获取blockNumber块的哈希 前256个块之内
41 COINBASE address 获取挖出当前区块的矿工地址
42 TIMESTAMP timestamp 获取当前区块时间戳
43 NUMBER blockNumber 获取当前区块号
44 PREVRANDAO prevRando 获取上个区块的RANDO mix 随机数
45 GASLIMIT gasLimit 获取当前区块的gas限制
46 CHAINID chainId 获取当前的链id
47 SELFBALANCE balance 获取当前账户的余额
48 BASEFEE baseFee 获取当前gas基础费用
50 POP y 从栈中弹出第一项
51 MLOAD offset value 从内存中压入栈数据 内存中offset字节开始的32字节,超出内存部分补0
52 MSTORE offset value 保存数据到内存中 从内存的第offset位开始保存32个字节数据,高位不足以0补齐
53 MSTORE8 offset value 保存数据到内存中 从内存的第offset位开始保存1个字节数据
54 SLOAD key value 将storage中键x对应的值压入栈中
55 SSTORE key value 将栈中键key和值value存入storage
56 JUMP counter 跳转到counter字节码处 counter处必须为JUMPDEST
57 JUMPI counter b 条件跳转 只有当b!=0时才进行跳转
58 PC counter 将当前程序计数器压入栈中
59 MSIZE size 将当前内存长度压入栈中 内存最大索引+1
5A GAS gas 获取当前可使用的gas
5B JUMPDEST JUMP跳转处占位符
60 PUSH1 value 将数据压入栈中 将PUSH后跟的一个字节数据压入栈中
61 PUSH2 value 将PUSH后跟的两个字节数据压入栈中
62 PUSH3 value
63 PUSH4 value
64 PUSH5 value
65 PUSH6 value
66 PUSH7 value
67 PUSH8 value
68 PUSH9 value
69 PUSH10 value
6A PUSH11 value
6B PUSH12 value
6C PUSH13 value
6D PUSH14 value
6E PUSH15 value
6F PUSH16 value
70 PUSH17 value
71 PUSH18 value
72 PUSH19 value
73 PUSH20 value
74 PUSH21 value
75 PUSH22 value
76 PUSH23 value
77 PUSH24 value
78 PUSH25 value
79 PUSH26 value
7A PUSH27 value
7B PUSH28 value
7C PUSH29 value
7D PUSH30 value
7E PUSH31 value
7F PUSH32 value 将数据压入栈中 将PUSH后跟的三十二个字节数据压入栈中
80 DUP1 value value value 复制栈中数据 复制栈中第一个数据到栈顶
81 DUP2 a b a b a 复制栈中第二个数据到栈顶
82 DUP3 ... value value ... value
83 DUP4 ... value value ... value
84 DUP5 ... value value ... value
85 DUP6 ... value value ... value
86 DUP7 ... value value ... value
87 DUP8 ... value value ... value
88 DUP9 ... value value ... value
89 DUP10 ... value value ... value
8A DUP11 ... value value ... value
8B DUP12 ... value value ... value
8C DUP13 ... value value ... value
8D DUP14 ... value value ... value
8E DUP15 ... value value ... value
8F DUP16 ... value value ... value 复制栈中数据 复制栈中第十六个数据到栈顶
90 SWAP1 a b b a 交换栈中数据 交换栈顶数据与栈顶后第一个字节数据(第二个数据)
91 SWAP2 a b c c b a 交换栈中数据 交换栈顶数据与栈顶后第二个字节数据(第三个数据)
92 SWAP3 a ... b b ... a
93 SWAP4 a ... b b ... a
94 SWAP5 a ... b b ... a
95 SWAP6 a ... b b ... a
96 SWAP7 a ... b b ... a
97 SWAP8 a ... b b ... a
98 SWAP9 a ... b b ... a
99 SWAP10 a ... b b ... a
9A SWAP11 a ... b b ... a
9B SWAP12 a ... b b ... a
9C SWAP13 a ... b b ... a
9D SWAP14 a ... b b ... a
9E SWAP15 a ... b b ... a
9F SWAP16 a ... b b ... a 交换栈中数据 交换栈顶数据与栈顶后第十六个字节数据(第十七个数据)
A0 LOG0 offset size 记录日志 从内存offset处复制size大小的数据记录日志,无主题
A1 LOG1 offset size topic 记录日志 一个主题
A2 LOG2 offset size topic1 topic2 记录日志 两个主题
A3 LOG3 offset size topic1 topic2 topic3 记录日志 三个主题
A4 LOG4 offset size topic1 topic2 topic3 topic4 记录日志 四个主题
F0 CREATE value offset size address 创建地址 通过address = keccak256(rlp([sender_address,sender_nonce]))[12:]创建地址<br />value: 提供的gas费<br />从内存中offset索引处开始的offSet个字节数据作为初始代码
F1 CALL gas address value argsOffset argsSize retOffset retSize success 调用函数 gas:提供gas费<br />address:函数地址<br />value:提供以太坊数<br />内存中argsOffset索引开始的argsSize个字节数据作为参数<br />内存中retOffset索引开始的retSize个字节作为返回参数存储位置
F2 CALLCODE gas address value argsOffset argsSize retOffset retSize success 不改变合约上下文调用函数 改变全局变量msg
F3 RETURN offset size 返回 停止执行并将内存中offset索引开始的size大小的数据作为返回参数
F4 DELEGATECALL gas address value argsOffset argsSize retOffset retSize success 不改变合约上下文调用函数 不改变全局变量msg
F5 CREATE2 value offset size salt address 创建地址 通过<br />initialisation_code = memory[offset:offset+size]<br />address = keccak256(0xff + sender_address + salt + keccak256(initialisation_code))[12:]<br />创建地址<br />value: 提供的gas费<br />从内存中offset索引处开始的offSet个字节数据作为初始代码<br />salt:函数中的盐参数
FA STATICCALL gas address argsOffset argsSize retOffset retSize success 静态函数调用 在调用的函数中不允许更改状态与发送以太坊
FD REVERT offset size 回滚 停止执行并回滚状态但返回数据和剩余gas
FE INVALID 无效指令 等同任何没被提到的指令,也等同于 REVERT(栈中0,0)
FF SELFDESTRUCT address 自毁 停止执行并删除当前地址,将剩余余额发送给address地址

评论