以下都是來自我的新書《解密EVM機制及合約安全漏洞》裏的內容
電子版PDF下載:https://download.csdn.net/download/softgmx/10800947
研究環境:
OS |
ubuntu 16.04 |
VM及合約語言 |
EVM/ solidity |
合約調試器 |
https://remix.ethereum.org |
Ethererum源碼 |
go語言版本的 |
EVM機制原理
智能合約容易產生漏洞的主要原因:
- 開發人員對EVM的運行機制不瞭解
- stack、memory和storage是怎樣存儲數據的
- 合約間調用是怎樣實現的,傳參和返回值又是怎樣在合約間傳遞的
- 用鏈上數據做隨機數種子時,應注意僞隨機的問題(鏈上數據是可見的,合約裏定義的私有變量實際是公開可見的,且一些字段是可以被礦工操縱的)
- solidity封裝好的區塊訪問方法實際上是讀取區塊鏈的哪部分數據
- 區塊鏈中交易費用出價高者可以插隊優先交易
- gas的消耗實現機制
- 開發人員對solidity編譯器的一些內部處理不瞭解
- send、transfer和call底層轉賬方法的實現原理和區別
- fallback機制的實現原理
- storage變量的存儲和索引原理
- 開發人員對solidity語言不熟悉
- 構造函數
- 鑑權方法
- 日誌記錄
- 算數溢出
這樣我們分析問題可以抽象出的三個層次來研究,如下圖:
圖一 智能合約的層次
以太坊的智能合約機(EVM)構成及工作原理:
每次我們call一個合約方法時,在call的函數實現裏,首先會創建一個contract 類對象,並填充對應的字段值(code,CallerAddress,Input,gas,value,selfAddress),然後把這個contract對象傳入EVM的解釋器Interpeter進行逐條指令的解釋執行,但在開始解釋之前,會生成一個新的stack和一個memory對象以用於後面程序的運行,並把結果寫入合約地址對應的StateDB。
圖二 EVM的工作原理
下面列出了EVM的幾個關鍵類的定義:
圖三 對應的類圖
圖四 EVM執行智能合約中function的過程
EVM三大核心部件:
- stack
- memory
- storage
Stack的實現:
type Stack struct {
data []*big.Int
}
- 最小單元32字節(最小對齊單位)
- 初始容量1024個元素
- 以動態數組方式實現,理論上可擴容(但實際上一個方法只有1024個元素可用)
- 主要指令push、pop、swap 、dup
- 數據不持久化
- 合約間調用,方法之間不共用棧(一個方法分配一個棧)
- 同一合約的方法調用不產生call, 直是簡單的 jump
Memory的實現:
type Memory struct {
store []byte
lastGasCost uint64
}
- 最小單位有一個字節
- 以動態字節數組方式實現,可以擴容
- 類似x86架構裏的堆,數據不具有持久性,合約執行完成,數據消失
- 主要指令mload、mstore
Storage的實現:
- 通過StateDB把數據存儲在區塊鏈上
- 主要指令sload、sstore
- 數據被持久化(寫在鏈上了,並在所有礦機上同步)
- 容量是2的256次冪,storage[slot]=value, key和value都是32位對齊的
slot=[ 0x0000000000000000000000000000000000000000000000000000000000000000,……
,0xffffffff ffffffff ffffffff ffffffff ffffffff ffffffff ffffffff ffffffff]
命令集分類:
- 算術、邏輯運算(add、sub、mul、div、mod、lt ,gt、 not、xor、or、and等)
- 棧操作、內存操作、存儲操作(push、pop、mload、mstore、sload、sstore)
- 區塊鏈操作(address、balance、gas、caller、callvalue、origin、blockhash、timestamp、difficulty、gaslimit、coinbase、log等)
- 執行流操作(jump、jumpi、call、callcode、delegatecall、 staticcall)
命令集具體說明:
Instruction |
|
|
Explanation |
stop |
- |
F |
stop execution, identical to return(0,0) |
add(x, y) |
|
F |
x + y |
sub(x, y) |
|
F |
x - y |
mul(x, y) |
|
F |
x * y |
div(x, y) |
|
F |
x / y |
sdiv(x, y) |
|
F |
x / y, for signed numbers in two’s complement |
mod(x, y) |
|
F |
x % y |
smod(x, y) |
|
F |
x % y, for signed numbers in two’s complement |
exp(x, y) |
|
F |
x to the power of y |
not(x) |
|
F |
~x, every bit of x is negated |
lt(x, y) |
|
F |
1 if x < y, 0 otherwise |
gt(x, y) |
|
F |
1 if x > y, 0 otherwise |
slt(x, y) |
|
F |
1 if x < y, 0 otherwise, for signed numbers in two’s complement |
sgt(x, y) |
|
F |
1 if x > y, 0 otherwise, for signed numbers in two’s complement |
eq(x, y) |
|
F |
1 if x == y, 0 otherwise |
iszero(x) |
|
F |
1 if x == 0, 0 otherwise |
and(x, y) |
|
F |
bitwise and of x and y |
or(x, y) |
|
F |
bitwise or of x and y |
xor(x, y) |
|
F |
bitwise xor of x and y |
byte(n, x) |
|
F |
nth byte of x, where the most significant byte is the 0th byte |
shl(x, y) |
|
C |
logical shift left y by x bits |
shr(x, y) |
|
C |
logical shift right y by x bits |
sar(x, y) |
|
C |
arithmetic shift right y by x bits |
addmod(x, y, m) |
|
F |
(x + y) % m with arbitrary precision arithmetics |
mulmod(x, y, m) |
|
F |
(x * y) % m with arbitrary precision arithmetics |
signextend(i, x) |
|
F |
sign extend from (i*8+7)th bit counting from least significant |
keccak256(p, n) |
|
F |
keccak(mem[p…(p+n))) |
sha3(p, n) |
|
F |
keccak(mem[p…(p+n))) |
jump(label) |
- |
F |
jump to label / code position |
jumpi(label, cond) |
- |
F |
jump to label if cond is nonzero |
pc |
|
F |
current position in code |
pop(x) |
- |
F |
remove the element pushed by x |
dup1 … dup16 |
|
F |
copy ith stack slot to the top (counting from top) |
swap1 … swap16 |
* |
F |
swap topmost and ith stack slot below it |
mload(p) |
|
F |
mem[p..(p+32)) |
mstore(p, v) |
- |
F |
mem[p..(p+32)) := v |
mstore8(p, v) |
- |
F |
mem[p] := v & 0xff (only modifies a single byte) |
sload(p) |
|
F |
storage[p] |
sstore(p, v) |
- |
F |
storage[p] := v |
msize |
|
F |
size of memory, i.e. largest accessed memory index |
gas |
|
F |
gas still available to execution |
address |
|
F |
address of the current contract / execution context |
balance(a) |
|
F |
wei balance at address a |
caller |
|
F |
call sender (excluding delegatecall) |
callvalue |
|
F |
wei sent together with the current call |
calldataload(p) |
|
F |
call data starting from position p (32 bytes) |
calldatasize |
|
F |
size of call data in bytes |
calldatacopy(t, f, s) |
- |
F |
copy s bytes from calldata at position f to mem at position t |
codesize |
|
F |
size of the code of the current contract / execution context |
codecopy(t, f, s) |
- |
F |
copy s bytes from code at position f to mem at position t |
extcodesize(a) |
|
F |
size of the code at address a |
extcodecopy(a, t, f, s) |
- |
F |
like codecopy(t, f, s) but take code at address a |
returndatasize |
|
B |
size of the last returndata |
returndatacopy(t, f, s) |
- |
B |
copy s bytes from returndata at position f to mem at position t |
create(v, p, s) |
|
F |
create new contract with code mem[p..(p+s)) and send v wei and return the new address |
create2(v, n, p, s) |
|
C |
create new contract with code mem[p..(p+s)) at address keccak256(<address> . n . keccak256(mem[p..(p+s))) and send v wei and return the new address |
call(g, a, v, in, insize, out, outsize) |
|
F |
call contract at address a with input mem[in..(in+insize)) providing g gas and v wei and output area mem[out..(out+outsize)) returning 0 on error (eg. out of gas) and 1 on success |
callcode(g, a, v, in, insize, out, outsize) |
|
F |
identical to call but only use the code from a and stay in the context of the current contract otherwise |
delegatecall(g, a, in, insize, out, outsize) |
|
H |
identical to callcode but also keep caller and callvalue |
staticcall(g, a, in, insize, out, outsize) |
|
B |
identical to call(g, a, 0, in, insize, out, outsize) but do not allow state modifications |
return(p, s) |
- |
F |
end execution, return data mem[p..(p+s)) |
revert(p, s) |
- |
B |
end execution, revert state changes, return data mem[p..(p+s)) |
selfdestruct(a) |
- |
F |
end execution, destroy current contract and send funds to a |
invalid |
- |
F |
end execution with invalid instruction |
log0(p, s) |
- |
F |
log without topics and data mem[p..(p+s)) |
log1(p, s, t1) |
- |
F |
log with topic t1 and data mem[p..(p+s)) |
log2(p, s, t1, t2) |
- |
F |
log with topics t1, t2 and data mem[p..(p+s)) |
log3(p, s, t1, t2, t3) |
- |
F |
log with topics t1, t2, t3 and data mem[p..(p+s)) |
log4(p, s, t1, t2, t3, t4) |
- |
F |
log with topics t1, t2, t3, t4 and data mem[p..(p+s)) |
origin |
|
F |
transaction sender |
gasprice |
|
F |
gas price of the transaction |
blockhash(b) |
|
F |
hash of block nr b - only for last 256 blocks excluding current |
coinbase |
|
F |
current mining beneficiary |
timestamp |
|
F |
timestamp of the current block in seconds since the epoch |
number |
|
F |
current block number |
difficulty |
|
F |
difficulty of the current block |
gaslimit |
|
F |
block gas limit of the current block |
storage、memory和stack操作的gas花費對比:
指令 |
對應宏定義 |
消耗gas數量 |
創建合約 |
TxGasContractCreation |
53000 |
創建新賬戶(對方地址不存在) |
CallNewAccountGas |
25000 |
SSTORE |
SstoreSetGas |
20000 |
MSTORE |
MemoryGas |
3*N(N爲有多少個32字節) |
PUSH(1…N) |
GasFastestStep |
3 |