通过ContractFuzzer来理解以太坊Fuzz技术
在部署到以太坊网络之前,智能合约需要经过编译生成字节码(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);
我们可以通过solc本地编译智能合约,得到相应的 bytecode、abi 以及 语法树 信息,语法树可以帮助我们进行数据流分析,但是在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生成的语法树进一步封装,具体可看数据流分析章节
以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
,其中6057361d
为store(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压入栈中因此我们发现:
FUZZ最终检测漏洞时,需要提供整个操作流程的某些信息,如分析重入漏洞时,需要函数调用栈、操作码栈等信息,这些EVM并不关心也没有提供原型供直接调用,因此我们需要对每个对EVM环境产生影响的操作进行hook,具体来说,就是在每个操作码执行的前后插入我们的代码。
对于EVM解析操作码的流程进行简要分析,大致如下:
判别操作码 → 调用操作码函数 → 根据pc跳转下一操作码 → 遇到结束操作码 → 返回
具体代码可关注/core/vm/
下文件:
228 ret, err = evm.interpreter.Run(contract, input, false)
调用解释器运行字节码
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++,遇到结束操作码退出执行
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来理解以太坊Fuzz的具体操作。ContractFuzzer是2018年被提出的一项智能合约Fuzz工具,可以在没有源码的条件下,基于智能合约的abi与bin文件进行多种漏洞的检测。作为该领域的先驱工具之一,其主体框架与思路目前仍受到大量工具借鉴与使用,具有较高的学习价值。经过笔者分析,该工具很好的封装了以太坊解释器,提供全局的函数调用栈、操作码栈、异常处理栈等供直接使用,前端代理合约采用巧妙的传参方式固定了重入合约的调用入口,主体主要分为3个部分:
该文件夹下仅提供工具依赖的dockerfile,无价值
该文件夹提供了简单的测试合约,供使用者快速体验工具的整体流程,以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记录合约名,无实际作用
该文件夹下为一些python编写的简单合约分析工具:
从etherscan下载智能合约的abi与bin信息
封装了excel的读写操作
从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文件,因此只需简单的反序列化操作即可
从bin中读取对应的函数签名对,函数签名对即每个函数的签名与该函数调用的函数签名列表,具体步骤:
evm disasm
将字节码转换为操作码该文件夹下为私链部署配置文件与持久化信息
私链启动命令:
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文件中获取
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
该部分基于以太坊源码,修改了EVM部分代码,hook了操作码解析流程,主要改动集中在 core/vm 文件夹下
以太坊在发生交易时调用StateTransition.TransitionDb()
函数修改数据库中链上交易信息,其中调用evm.Call()
函数调用合约,根据前置知识分析,主要修改evm.go
与interpreter.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状态改变前先修改相应的全局变量
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:是否余额不足
创建新的HackerContractCall,初始化操作码栈,第一项为传入的operartion;初始化状态栈为空;初始化nextcalls为空;初始化input参数
在 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)
}
在 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()
在evm.Call()/DelegateCall()/CallCode() 中调用,创建新的HackerContractCall,并压入函数调用栈
除 Call/DelegateCall/CallCode 外的所有操作码,均仅执行两步操作:向操作码栈中压入本操作码,向状态栈中压入调用者与被调用者的状态体
call.OperationStack.push(opCodeToString[RETURN])
call.StateStack.push(newHackerState(call.caller, call.callee))
代理操作码处理逻辑,主要功能在执行原有操作码功能前,先调用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)
}
封装操作码栈,记录测试过程中每个操作码的调用顺序
type HackerOperationStack struct {
data []string
}
主要封装函数调用过程中各合约状态
type Hacker_ContractState struct {
addr common.Address
storage map[common.Hash]common.Hash
balance big.Int
}
封装合约的地址、存储信息、余额信息
type HackerState struct {
contracts []*Hacker_ContractState
}
封装每次函数调用时调用者与被调用者的状态对
创建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}
}
type HackerStateStack struct {
data []*HackerState
}
封装整个测试过程中调状态用对栈
主要封装一些工具类函数
判断两个合约地址是否一致,包括balance与storage的差异
对一次函数调用进行哈希,哈希值=Hash(caller)+Hash(callee)+Hash(input)
判断地址是否与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
}
}
提供17种漏洞测试机,对于不同条件检测并生成测试报告
type Oracle interface {
InitOracle(hacker_call_hashs []common.Hash, hacker_calls []*HackerContractCall)
TestOracle() bool
Write(writer io.Writer)
String() string
}
提供测试机的基本接口函数,其中InitOracle()与TestOracle()都在hacker_close()函数中调用
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
}
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
}
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
}
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
}
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
}
type HackerCallEtherTransferFailed struct {
hacker_call_hashs []common.Hash
hacker_calls []*HackerContractCall
hacker_fallback_calls []*HackerContractCall
}
TestOracle():
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
}
type HackerGaslessSend struct {
hacker_call_hashs []common.Hash
hacker_calls []*HackerContractCall
hacker_exception_calls []*HackerContractCall
}
TestOracle():
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
}
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])
}
异常无序指多个函数调用中,底层函数抛出的异常未被顶层函数捕获
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
}
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
}
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
}
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
}
函数调用可控指函数调用中被调用者地址或函数传参为用户可控
type HackerUnknownCall struct {
hacker_call_hashs []common.Hash
hacker_calls []*HackerContractCall
hacker_exception_calls []*HackerContractCall
}
TestOracle():
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))
}
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
}
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])
}
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])
}
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])
}
type HackerCallInfoReportor struct {
hacker_call_hashs []common.Hash
hacker_calls []*HackerContractCall
callsLen int
root *HackerContractCall
operationLen int
operationStack *HackerOperationStack
}
主要包括:
通过Profile()生成结果,在hacker_close()中被调用,作为完整调用信息发送给fuzzServer
该模块主要通过合约的abi生成fuzz测试序列,主题文件在 src/ContractFuzzer
文件夹下
对abi_dir下的文件进行fuzz:
go fuzz.Start(*abi_dir,*out_dir)
开启fuzzServer:
go server.Start(*addr_map,*reporter)
封装abi文件的序列化与反序列化等操作
不同type的fuzz种子序列,如地址的种子列表:
{"name":"AddressSeeds","seeds":["0xe930e50b62af818dbc955f345f9a3a3108f7a70d"],"seeds1":["0xe930e50b62af818dbc955f345f9a3a3108f7a70d"],"seeds2":["0x952fa21849f5e6ce0c3a233f6036caabb9e944e2","0xd9e2a443db97d545619ee4afa82535e506ed0913","0xc77c361688d6a81fd2b7a003ac599412ad9da854",......]}
其中0xe930e50b62af818dbc955f345f9a3a3108f7a70d
为代理合约地址,可供重入漏洞测试
从所有函数列表中,生成随机数选取入口函数:
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"]}]
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())
启动fuzzServer,默认监听地址为8888,接收合约执行结束后的测试结果,处理器为hackHandler()函数
633 http.HandleFunc("/hack", hackHandler)
634 log.Fatal(http.ListenAndServe(fuzz.Global_listen_port, nil))
该模块负责中转fuzz客户端与私链的通信,插入代理合约的调用
启动命令:
#!/bin/sh
bnode ./utils/runFuzzMonitor --ip http://127.0.0.1:8545 --account 0 --value 0
ip参数为私链rpc地址,默认端口为8545
首先通过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);
重新模拟初始调用,即重入函数
默认监听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/');
}
发送三次交易:
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);
}
AgentCallWithValue()
函数初始化合约变量,供重入测试该模块负责将测试合约部署在私链上
#!/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部署合约
主要配置私链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
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<b |
小于 | 结果为 1/0 |
11 | GT | a b |
a>b |
大于 | |
12 | SLT | a b |
a<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 << 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地址 |
如果觉得我的文章对您有用,请随意打赏。你的支持将鼓励我继续创作!