当前位置:文档之家› 以太坊安全之EVM与短地址攻击

以太坊安全之EVM与短地址攻击

以太坊安全之EVM与短地址攻击
以太坊安全之EVM与短地址攻击

前言

以太坊(Ethereum)是一个开源的有智能合约功能的公共区块链平台,通过其专用加密货币

以太币(ETH)提供去中心化的以太坊虚拟机(EVM)来处理点对点合约。EVM(Ethereum)虚拟机),以太坊虚拟机的简称,以太坊的核心之一。智能合约的创建和执行都由EVM来

完成,简单来说,EVM是一个状态执行的机器,输入是solidity编译后的二进制指令和例程

的状态数据,输出是异步状态的改变。

以太坊短地址攻击,最初由Golem团队于2017年4月提出,是由于EVM的设计缺陷导致的

突破。ERC20代币标准定义的转账函数如下:

function transfer(address to uint256 value) public returns (bool success)

如果前缀的to是末尾前缀的短地址,则EVM在后面字节补足地址,而最后的value值不足

则用0填充,导致实际转出的代币数值倍增。

本文从以太坊原始代码的角度分析EVM重叠是如何处理执行智能合约字节码的,并简要分

析短地址攻击的原理。

EVM原始分析

evm.go

EVM源码的位于go-ethereum/core/vm/目录下,在evm.go中定义了EVM结构体,并实现了EVM.Call,EVM.CallCode,EVM.DelegateCall,EVM.StaticCall四种方法来调用智能合约,EVM.Call实现了基本的合约调用的功能,三种后面与方法EVM.Call略有区别,但最终都调

用run函数来解析执行智能合约

EVM呼叫

// Call executes the contract associated with the addr with the given input as

// parameters. It also handles any necessary value transfer required and takes

// the necessary steps to create accounts and reverses the state in case of an

// execution error or failed value transfer.

//hunya// 基本的合约调用

func (evm *EVM) Call(caller ContractRef addr common.Address input []byte gas uint64 value

*big.Int) (ret []byte leftOverGas uint64 err error) { if evm.vmConfig.NoRecursion && evm.depth > 0 { return nil gas nil }

// Fail if we're trying to execute above the call depth limit

if evm.depth > int(params.CallCreateDepth) { return nil gas ErrDepth } // Fail if we're trying to transfer more than the available balance

if !evm.Context.CanTransfer(evm.StateDB caller.Address() value) { return nil gas ErrInsufficientBalance }

var ( to = AccountRef(addr) snapshot = evm.StateDB.Snapshot() )

if !evm.StateDB.Exist(addr) { precompiles := PrecompiledContractsHomestead if

evm.chainRules.IsByzantium { precompiles = PrecompiledContractsByzantium } if evm.chainRules.IsIstanbul { precompiles = PrecompiledContractsIstanbul } if precompiles[addr] == nil && evm.chainRules.IsEIP158 && value.Sign() == 0 { // Calling a non existing account don't do anything but ping the tracer

if evm.vmConfig.Debug && evm.depth == 0

{ evm.vmConfig.Tracer.CaptureStart(caller.Address() addr false input gas value) evm.vmConfig.Tracer.CaptureEnd(ret 0 0 nil) } return nil gas nil }

evm.StateDB.CreateAccount(addr) } evm.Transfer(evm.StateDB caller.Address() to.Address() value) // Initialise a new contract and set the code that is to be used by the EVM.

// The contract is a scoped environment for this execution context only.

contract := NewContract(caller to value gas) contract.SetCallCode(&addr

evm.StateDB.GetCodeHash(addr) evm.StateDB.GetCode(addr))

// Even if the account has no code we need to continue because it might be a precompile

start := time.Now()

// Capture the tracer start/end events in debug mode

// debug模式会捕获tracer的start/end事件

if evm.vmConfig.Debug && evm.depth == 0

{ evm.vmConfig.Tracer.CaptureStart(caller.Address() addr false input gas value)

defer func() { // Lazy evaluation of the parameters

evm.vmConfig.Tracer.CaptureEnd(ret gas-contract.Gas time.Since(start) err) }() }

ret err = run(evm contract input false)//hunya// 调用run函数执行合约

// When an error was returned by the EVM or when setting the creation code

// above we revert to the snapshot and consume any gas remaining. Additionally

// when we're in homestead this also counts for code storage gas errors.

if err != nil { evm.StateDB.RevertToSnapshot(snapshot) if err != errExecutionReverted { https://www.doczj.com/doc/6e11115095.html,eGas(contract.Gas) } } return ret contract.Gas err

}

EVM.CallCode

// CallCode executes the contract associated with the addr with the given input

// as parameters. It also handles any necessary value transfer required and takes

// the necessary steps to create accounts and reverses the state in case of an

// execution error or failed value transfer.

//

// CallCode differs from Call in the sense that it executes the given address'

// code with the caller as context.

//hunya// 类似solidity中的call函数,调用外部合约,执行上下文在被调用合约中

func (evm *EVM) CallCode(caller ContractRef addr common.Address input []byte gas uint64 value *big.Int) (ret []byte leftOverGas uint64 err error) { if evm.vmConfig.NoRecursion && evm.depth > 0 { return nil gas nil }

// Fail if we're trying to execute above the call depth limit

if evm.depth > int(params.CallCreateDepth) { return nil gas ErrDepth } // Fail if we're trying to transfer more than the available balance

if !evm.CanTransfer(evm.StateDB caller.Address() value) { return nil gas ErrInsufficientBalance }

var ( snapshot = evm.StateDB.Snapshot() to = AccountRef(caller.Address()) ) // Initialise a new contract and set the code that is to be used by the EVM.

// The contract is a scoped environment for this execution context only.

contract := NewContract(caller to value gas) contract.SetCallCode(&addr

evm.StateDB.GetCodeHash(addr) evm.StateDB.GetCode(addr))

ret err = run(evm contract input false)//hunya// 调用run函数执行合约

if err != nil { evm.StateDB.RevertToSnapshot(snapshot) if err != errExecutionReverted { https://www.doczj.com/doc/6e11115095.html,eGas(contract.Gas) } } return ret contract.Gas err

}

EVM.DelegateCall

// DelegateCall executes the contract associated with the addr with the given input

// as parameters. It reverses the state in case of an execution error.

//

// DelegateCall differs from CallCode in the sense that it executes the given address'

// code with the caller as context and the caller is set to the caller of the caller.

//hunya// 类似solidity中的delegatecall函数,调用外部合约,执行上下文在调用合约中

func (evm *EVM) DelegateCall(caller ContractRef addr common.Address input []byte gas uint64) (ret []byte leftOverGas uint64 err error) { if evm.vmConfig.NoRecursion && evm.depth > 0 { return nil gas nil } // Fail if we're trying to execute above the call depth limit

if evm.depth > int(params.CallCreateDepth) { return nil gas ErrDepth }

var ( snapshot = evm.StateDB.Snapshot() to = AccountRef(caller.Address()) )

// Initialise a new contract and make initialise the delegate values

contract := NewContract(caller to nil gas).AsDelegate() contract.SetCallCode(&addr

evm.StateDB.GetCodeHash(addr) evm.StateDB.GetCode(addr))

ret err = run(evm contract input false)//hunya// 调用run函数执行合约

if err != nil { evm.StateDB.RevertToSnapshot(snapshot) if err != errExecutionReverted { https://www.doczj.com/doc/6e11115095.html,eGas(contract.Gas) } } return ret contract.Gas err

}

EVM.StaticCall

// StaticCall executes the contract associated with the addr with the given input

// as parameters while disallowing any modifications to the state during the call.

// Opcodes that attempt to perform such modifications will result in exceptions

// instead of performing the modifications.

//hunya// 与EVM.Call类似,但不允许执行会修改永久存储的数据的指令

func (evm *EVM) StaticCall(caller ContractRef addr common.Address input []byte gas uint64) (ret []byte leftOverGas uint64 err error) { if evm.vmConfig.NoRecursion && evm.depth > 0

{ return nil gas nil } // Fail if we're trying to execute above the call depth limit

if evm.depth > int(params.CallCreateDepth) { return nil gas ErrDepth }

var ( to = AccountRef(addr) snapshot = evm.StateDB.Snapshot() ) // Initialise a new contract and set the code that is to be used by the EVM.

// The contract is a scoped environment for this execution context only.

contract := NewContract(caller to new(big.Int) gas) contract.SetCallCode(&addr

evm.StateDB.GetCodeHash(addr) evm.StateDB.GetCode(addr))

// We do an AddBalance of zero here just in order to trigger a touch.

// This doesn't matter on Mainnet where all empties are gone at the time of Byzantium

// but is the correct thing to do and matters on other networks in tests and potential

// future scenarios

evm.StateDB.AddBalance(addr bigZero)

// When an error was returned by the EVM or when setting the creation code

// above we revert to the snapshot and consume any gas remaining. Additionally

// when we're in Homestead this also counts for code storage gas errors.

ret err = run(evm contract input true)//hunya// 调用run函数执行合约

if err != nil { evm.StateDB.RevertToSnapshot(snapshot) if err != errExecutionReverted { https://www.doczj.com/doc/6e11115095.html,eGas(contract.Gas) } } return ret contract.Gas err

}

run函数前半段是判断是否基于太坊内置预编译的特殊合约,有单独的运行方式

后半段则是对于一般的合约调用解释器interpreter去执行调用

[MISSING IMAGE: , ]

interpreter.go

解释器相关代码在interpreter.go中,interpreter是一个接口,目前只有EVMInterpreter这一个具体实现

[MISSING IMAGE: , ]

合约EVM.Call汇率引入Interpreter.Run来到EVMInpreter.Run

EVMInterpreter的Run方法代码编码,其中处理执行合约字节码的主循环如下:[MISSING IMAGE: , ]

大部分代码主要是检查准备运行环境,执行合约字节码的核心代码主要是以下3行

op = contract.GetOp(pc)

operation := in.cfg.JumpTable[op]

......

res err = operation.execute(&pc in contract mem stack)

......

interpreter的主要工作实际上只是通过JumpTable查找指令,从而实现一个翻译解析的作用最终的执行是通过调用operation对象的execute方法

jump_table.go

operation的定义放在jump_table.go中

[MISSING IMAGE: , ]

jump_table.go中还定义了JumpTable和多种不同的指令集

[MISSING IMAGE: , ]

在基本指令集中有三个处理input的指令,分别是CALLDATALOAD,CALLDATASIZE和CALLDATACOPY

[MISSING IMAGE: , ]

jump_table.go中的代码同样只是决议解析的功能,提供了指令的查找,定义了每个指令具体的执行函数

Instructions.go

instructions.go中是所有指令的具体实现,上述三个函数的具体实现如下:

[MISSING IMAGE: , ]

这三个函数的作用分别是从input加载参数入栈,获取input大小,复制input中的参数到内存

我们重点关注opCallDataLoad函数是如何处理input中的参数入栈的

opCallDataLoad函数调用getDataBig函数,预期contract.Input,stack.pop()和big32,将结果转为big.Int入栈

[MISSING IMAGE: , ]

getDataBig函数以stack.pop()栈顶元素作为初始索引,截取input中big32大小的数据,然后初始化common.RightPadBytes处理并返回

其中涉及到的另外两个函数math.BigMin和common.RightPadBytes如下:

//file: go-thereum/common/math/big.go

func BigMin(x y *big.Int) *big.Int { if x.Cmp(y) > 0 { return y } return x

}

//file: go-ethereum/common/bytes.go

func RightPadBytes(slice []byte l int) []byte { if l <= len(slice) { return slice } //右填充

0x00至l位

padded := make([]byte l) copy(padded slice)

return padded

}

分析到这里,基本上已经能很明显看到问题所在了

RightPadBytes会将函数传入的字节切片向左向右填充至l位长度,而l的英文被传入的big32,即32位长度

所以在短地址攻击中,调用的transfer(address to uint256 value)函数,如果to是低位更改的地址,由于EVM在处理时是固定截取32位长度的,因此重复value数值高位补的0算进to的

末端,而在截取value时由于位数不足32位,则右填充0x00至32位,最终导致转账的value 指数级增长

测试与复现

编写一个简单的合约来测试

pragma solidity ^0.5.0;

contract Test { uint256 internal _totalSupply;

mapping(address => uint256) internal _balances;

event Transfer(address indexed from address indexed to uint256 value);

constructor() public { _totalSupply = 1 * 10 ** 18; _balances[msg.sender] =

_totalSupply; }

function totalSupply() external view returns (uint256) { return _totalSupply; }

function balanceOf(address account) external view returns (uint256) { return

_balances[account]; }

function transfer(address touint256 value) public returns (bool) { require(to != address(0)); require(_balances[msg.sender] >= value); require(_balances[to] + value >= _balances[to]);

_balances[msg.sender] -= value; _balances[to] += value; emit Transfer(msg.sender to value); }

}

混音部署,调用transfer发起正常的转账

[MISSING IMAGE: , ]

input为

0xa9059cbb00000000000000000000000071430fd8c82cc7b991a8455fc6ea5b37a06d393f000000000 0000000000000000000000000000000000000000000000000000001

直接尝试短地址攻击,减少去转账地址的后两位,会发现并不能通过,remix会直接报错[MISSING IMAGE: , ]

这是因为web3.js做了验证,web3.js是用户与以太坊互连相互的中介

原始码复现

通过二进制函数复现如下:

[MISSING IMAGE: , ]

实际复现

根据如何完成实际场景的攻击,可以参考文末的链接[1],利用

web3.eth.sendSignedTransaction绕过过限制

实际上,web3.js做的校准仅在显着式转移转账地址的函数,如web3.eth.sendTransaction这种,像web3.eth.sendSignedTransaction,web3.eth.sendRawTransaction这种指向的参数是序列化后

的数据的就校正不了,是可以完成短地址攻击的,兴趣的可以自己尝试,这里就不多写了PS:文中分析的go-ethereum原始码版本是commit-fdff182,原始码与最新版本有些出入,但

最新版本的也未修复这种缺陷(可能官方不认为这是缺陷?),分析思路依然可以沿用

思考

以太坊曾经EVM并没有修复短地址攻击的这么一个缺陷,而是直接在web3.js里对地址做的

校验,目前各种合约或多或少也做了弥补,所以虽然EVM可能可以复现,但实际场景中问

题应该不大,但如果是开放RPC的中断可能还是会存在这种风险

另外还有一个点,按一下EVM的这种机制,易受攻击的应该换一个transfer(address to

uint256 value)这个点,只是因为这个函数是ERC20代币标准,而且参数的设计恰好能导致涉

及金额的短地址攻击,并且在其他的一些非代币合约,如竞猜,游戏类的合约中,一些非转

账类的事务处理函数中,如果不对类似地址这,种的参数做长度校正,可能也存在类似的短

地址攻击的风险,也可能并不重复地址,可能还有其他的利用方式还没挖掘出来。

参考

[1]以太坊短地址攻击详解

[2]以太坊原始解析:evm

相关主题
文本预览
相关文档 最新文档