亚洲日本免费-啊轻点灬太粗太长了三男一女-麻豆av电影在线观看-日韩一级片毛片|www.grbbt.com

KCon議題解讀 | 以太坊智能合約 OPCODE 逆向之調(diào)試器篇

Remix調(diào)試器

Remix帶有一個(gè)非常強(qiáng)大的Debugger,當(dāng)我的調(diào)試器寫(xiě)到一半的時(shí)候,才發(fā)現(xiàn)了Remix自帶調(diào)試器的強(qiáng)大之處,本文首先,對(duì)Remix的調(diào)試器進(jìn)行介紹。

能調(diào)試的范圍:

1. 在Remix上進(jìn)行每一個(gè)操作(創(chuàng)建合約/調(diào)用合約/獲取變量值)時(shí),在執(zhí)行成功后,都能在下方的控制界面點(diǎn)擊DEBUG按鈕進(jìn)行調(diào)試

2. Debugger能對(duì)任意交易進(jìn)行調(diào)試,只需要在調(diào)試窗口輸入對(duì)應(yīng)交易地址

3. 能對(duì)公鏈,測(cè)試鏈,私鏈上的任意交易進(jìn)行調(diào)試

點(diǎn)擊Environment可以對(duì)區(qū)塊鏈環(huán)境進(jìn)行設(shè)置,選擇Injected Web3,環(huán)境取決去瀏覽器安裝的插件

比如我,使用的瀏覽器是Chrome,安裝的插件是MetaMask

通過(guò)MetaMask插件,我能選擇環(huán)境為公鏈或者是測(cè)試鏈,或者是私鏈

當(dāng)Environment設(shè)置為Web3 Provider可以自行添加以太坊區(qū)塊鏈的RPC節(jié)點(diǎn),一般是用于設(shè)置環(huán)境為私鏈

4. 在JavaScript的EVM環(huán)境中進(jìn)行調(diào)試

見(jiàn)3中的圖,把Environment設(shè)置為JavaScript VM則表示使用本地虛擬環(huán)境進(jìn)行調(diào)試測(cè)試

在調(diào)試的過(guò)程中能做什么?

Remix的調(diào)試器只提供了詳細(xì)的數(shù)據(jù)查看功能,沒(méi)法在特定的指令對(duì)STACK/MEM/STORAGE進(jìn)行操作

在了解清楚Remix的調(diào)試器的功能后,感覺(jué)我進(jìn)行了一半的工作好像是在重復(fù)造輪子。

之后仔細(xì)思考了我寫(xiě)調(diào)試器的初衷,今天的WCTF有一道以太坊智能合約的題目,因?yàn)榈谝淮握J(rèn)真的逆向EVM的OPCODE,不熟練,一個(gè)下午還差一個(gè)函數(shù)沒(méi)有逆向出來(lái),然后比賽結(jié)束了,感覺(jué)有點(diǎn)遺憾,如果當(dāng)時(shí)能動(dòng)態(tài)調(diào)試,可能逆向的速度能更快。

Remix的調(diào)試器只能對(duì)已經(jīng)發(fā)生的行為(交易)進(jìn)行調(diào)試,所以并不能滿足我打CTF的需求,所以對(duì)于我寫(xiě)的調(diào)試器,我轉(zhuǎn)換了一下定位:調(diào)試沒(méi)有源碼,只有OPCODE的智能合約的邏輯,或者可以稱為離線調(diào)試。

調(diào)試器的編寫(xiě)

智能合約調(diào)試器的編寫(xiě),我認(rèn)為最核心的部分是實(shí)現(xiàn)一個(gè)OPCODE解釋器,或者說(shuō)是自己實(shí)現(xiàn)一個(gè)EVM。

實(shí)現(xiàn)OPCODE解釋器又分為兩部分,1. 設(shè)計(jì)和實(shí)現(xiàn)數(shù)據(jù)儲(chǔ)存器(把STACK/MEM/STORAGE統(tǒng)稱為數(shù)據(jù)儲(chǔ)存器),2. 解析OPCODE指令

數(shù)據(jù)儲(chǔ)存器

STACK

根據(jù)OPCODE指令的情況,EVM的棧和計(jì)算機(jī)的棧數(shù)據(jù)結(jié)構(gòu)是一個(gè)樣的,先入先出,都有PUSH和POP操作。不過(guò)EVM的棧還多了SWAP和DUP操作,棧交換和棧復(fù)制,如下所示,是我使用Python實(shí)現(xiàn)的EVM棧類:

class STACK(Base):
    """
    evm stack
    """
    stack: [int]
    max_value: int
    def __init__(self):
        self.stack = []
        self.max_value = 2**256
    def push(self, data: int):
        """
        OPCODE: PUSH
        """
        self.stack.append(data % self.max_value)
    def pop(self) -> (int):
        """
        OPCODE POP
        """
        return self.stack.pop()
    @Base.stackcheck
    def swap(self, n):
        """
        OPCODE: SWAPn(1-16)
        """
        tmp = self.stack[-n-1]
        self.stack[-n-1] = self.stack[-1]
        self.stack[-1] = tmp
    @Base.stackcheck
    def dup(self, n):
        """
        OPCODE: DUPn(1-16)
        """
        self.stack.append(self.stack[-n])

和計(jì)算機(jī)的棧比較,我覺(jué)得EVM的棧結(jié)構(gòu)更像Python的List結(jié)構(gòu)

計(jì)算機(jī)的棧是一個(gè)地址儲(chǔ)存一個(gè)字節(jié)的數(shù)據(jù),取值可以精確到一個(gè)字節(jié),而EVM的棧是分塊儲(chǔ)存,每次PUSH占用一塊,每次POP取出一塊,每塊最大能儲(chǔ)存32字節(jié)的數(shù)據(jù),也就是2^256-1,所以上述代碼中,對(duì)每一個(gè)存入棧中的數(shù)據(jù)進(jìn)行取余計(jì)算,保證棧中的數(shù)據(jù)小于2^256-1

MEM

EVM的內(nèi)存的數(shù)據(jù)結(jié)構(gòu)幾乎和計(jì)算機(jī)內(nèi)存的一樣,一個(gè)地址儲(chǔ)存一字節(jié)的數(shù)據(jù)。在EVM中,因?yàn)闂5慕Y(jié)構(gòu),每塊儲(chǔ)存的數(shù)據(jù)最大為256bits,所以當(dāng)OPCODE指令需要的參數(shù)長(zhǎng)度可以大于256bits時(shí),將會(huì)使用到內(nèi)存

如下所示,是我使用Python實(shí)現(xiàn)的MEM內(nèi)存類:

class MEM(Base):
    """
    EVM memory
    """
    mem: bytearray
    max_value: int
    length: int
    def __init__(self):
        self.mem = bytearray(0)
        self.max_value = 2**256
        self.length = 0
        self.extend(1)
    @Base.memcheck
    def set(self, key: int, value: int):
        """
        OPCODE: MSTORE
        """
        value %= self.max
        self.mem[key: key+0x20] = value.to_bytes(0x20, "big")
        self.length += 0x20
    @Base.memcheck
    def set_byte(self, key: int, value: int):
        """
        OPCODE: MSTORE8
        """
        self.mem[key] = value  & 0xff
        self.length += length
    @Base.memcheck
    def set_length(self, key: int, value: int, length: int):
        """
        OPCODE: XXXXCOPY
        """
        value %= (2**(8*length))
        data = value.to_bytes(length, "big")
        self.mem[key: key+length] = data
        self.length += length
    @Base.memcheck
    def get(self, key: int) -> (int):
        """
        OPCODE: MLOAD
        return uint256
        """
        return int.from_bytes(self.mem[key: key+0x20], "big", signed=False)
    @Base.memcheck
    def get_bytearray(self, key: int) -> (bytearray):
        """
        OPCODE: MLOAD
        return 32 byte array
        """
        return self.mem[key: key+0x20]
    @Base.memcheck
    def get_bytes(self, key: int) -> (bytes):
        """
        OPCODE: MLOAD
        return 32 bytes
        """
        return bytes(self.mem[key: key+0x20])
    @Base.memcheck
    def get_length(self, key:int , length: int) -> (int):
        """
        return mem int value
        """
        return int.from_bytes(self.mem[key: key+length], "big", signed=False)
    @Base.memcheck
    def get_length_bytes(self, key:int , length: int) -> (bytes):
        """
        return mem bytes value
        """
        return bytes(self.mem[key: key+length])
    @Base.memcheck
    def get_length_bytearray(self, key:int , length: int) -> (bytearray):
        """
        return mem int value
        """
        return self.mem[key: key+length]
    def extend(self, num: int):
        """
        extend mem space
        """
        self.mem.extend(bytearray(256*num))

使用python3中的bytearray類型作為MEM的結(jié)構(gòu),默認(rèn)初始化256B的內(nèi)存空間,因?yàn)橛幸粋€(gè)OPCODE是MSIZE:

Get the size of active memory in bytes.

所以每次設(shè)置內(nèi)存值時(shí),都要計(jì)算active memory的size

內(nèi)存相關(guān)設(shè)置的指令分為三類

  1. MSTORE, 儲(chǔ)存0x20字節(jié)長(zhǎng)度的數(shù)據(jù)到內(nèi)存中
  2. MSTORE8, 儲(chǔ)存1字節(jié)長(zhǎng)度的數(shù)據(jù)到內(nèi)存中
  3. CALLDATACOPY(或者其他類似指令),儲(chǔ)存指定字節(jié)長(zhǎng)度的數(shù)據(jù)到內(nèi)存中

所以對(duì)應(yīng)的設(shè)置了3個(gè)不同的儲(chǔ)存數(shù)據(jù)到內(nèi)存中的函數(shù)。獲取內(nèi)存數(shù)據(jù)的類似。

STORAGE

EVM的STORAGE的數(shù)據(jù)結(jié)構(gòu)和計(jì)算機(jī)的磁盤(pán)儲(chǔ)存結(jié)構(gòu)相差就很大了,STORAGE是用來(lái)儲(chǔ)存全局變量的,全局變量的數(shù)據(jù)結(jié)構(gòu)我在上一篇文章中分析過(guò),所以在用Python實(shí)現(xiàn)中,我把STORAGE定義為了字典,相關(guān)代碼如下:

class STORAGE(Base):
    """
    EVM storage
    """
    storage: {str: int}
    max: int
    def __init__(self, data):
        self.storage = data
        self.max = 2**256
    @Base.storagecheck
    def set(self, key: str, value: int):
        self.storage[key] = value % self.max
    @Base.storagecheck
    def get(self, key: str) -> (int):
        return self.storage[key]

因?yàn)镋VM中操作STORAGE的相關(guān)指令只有SSTORE和SLOAD,所以使用python的dict類型作為STORAGE的結(jié)構(gòu)最為合適

解析OPCODE指令

對(duì)于OPCODE指令的解析難度不是很大,指令只占一個(gè)字節(jié),所以EVM的指令最多也就256個(gè)指令(0x00-0xff),但是有很多都是處于UNUSE,所以以后智能合約增加新指令后,調(diào)試器也要進(jìn)行更新,因此現(xiàn)在寫(xiě)的代碼需要具備可擴(kuò)展性。雖然解析指令的難度不大,但是仍然是個(gè)體力活,下面先來(lái)看看OPCODE的分類

OPCODE分類

在以太坊官方黃皮書(shū)中,對(duì)OPCODE進(jìn)行了相應(yīng)的分類:

0s: Stop and Arithmetic Operations (從0x00-0x0f的指令類型是STOP指令加上算術(shù)指令)

10s: Comparison & Bitwise Logic Operations (0x10-0x1f的指令是比較指令和比特位邏輯指令)

20s: SHA3 (目前0x20-0x2f只有一個(gè)SHA3指令)

30s: Environmental Information (0x30-0x3f是獲取環(huán)境信息的指令)

40s: Block Information (0x40-0x4f是獲取區(qū)塊信息的指令)

50s: Stack, Memory, Storage and Flow Operations (0x40-0x4f是獲取棧、內(nèi)存、儲(chǔ)存信息的指令和流指令(跳轉(zhuǎn)指令))

60s & 70s: Push Operations (0x60-0x7f是32個(gè)PUSH指令,PUSH1-PUSH32)

80s: Duplication Operations (0x80-0x8f屬于DUP1-DUP16指令)

90s: Exchange Operations (0x90-0x9f屬于SWAP1-SWAP16指令)

a0s: Logging Operations (0xa0-0xa4屬于LOG0-LOG4指令)

f0s: System operations (0xf0-0xff屬于系統(tǒng)操作指令)

設(shè)計(jì)可擴(kuò)展的解釋器

首先,設(shè)計(jì)一個(gè)字節(jié)和指令的映射表:

import typing

class OpCode(typing.NamedTuple):
    name: str
    removed: int            # 參數(shù)個(gè)數(shù)
    args: int               # PUSH根據(jù)該參數(shù)獲取opcode之后args字節(jié)的值作為PUSH的參數(shù)

_OPCODES = {
    '00': OpCode(name = 'STOP', removed = 0, args = 0),
    ......
}

for i in range(96, 128):
    _OPCODES[hex(i)[2:]] = OpCode(name='PUSH' + str(i - 95), removed=0, args=i-95)
......

# 因?yàn)榫幾g器優(yōu)化的問(wèn)題,OPCODE中會(huì)出現(xiàn)許多執(zhí)行不到的,UNUSE的指令,為防止解析失敗,還要對(duì)UNUSE的進(jìn)行處理
for i in range(0, 256):
    if not _OPCODES.get(hex(i)[2:].zfill(2)):
            _OPCODES[hex(i)[2:].zfill(2)] = OpCode('UNUSE', 0, 0)

然后就是設(shè)計(jì)一個(gè)解釋器類:

class Interpreter:
    """
    EVM Interpreter
    """
    MAX = 2**256
    over = 1
    store: EVMIO
    #############
    #  0s: Stop and Arithmetic Operations
    #############
    @staticmethod
    def STOP():
        """
        OPCODE: 0x00
        """
        Interpreter.over = 1
        print("========Program STOP=========")
    @staticmethod
    def ADD(x:int, y:int):
        """
        OPCODE: 0x01
        """
        r = (x + y) % Interpreter.MAX
        Interpreter.store.stack.push(r)
......
  • MAX變量用來(lái)控制計(jì)算的結(jié)果在256bits的范圍內(nèi)
  • over變量用來(lái)標(biāo)識(shí)程序是否執(zhí)行結(jié)束
  • store用來(lái)訪問(wèn)runtime變量: STACK, MEM, STORAGE

在這種設(shè)計(jì)模式下,當(dāng)解釋響應(yīng)的OPCODE,可以直接使用

args = [stack.pop() for _ in OpCode.removed]
getattr(Interpreter, OpCode.name)(*args)

特殊指令的處理思路

在OPCODE中有幾類特殊的指令:

1. 獲取區(qū)塊信息的指令,比如:

NUMBER: Get the block’s number

該指令是獲取當(dāng)前交易打包進(jìn)的區(qū)塊的區(qū)塊數(shù)(區(qū)塊高度),解決這個(gè)指令有幾種方案:

  • 設(shè)置默認(rèn)值
  • 設(shè)置一個(gè)配置文件,在配置文件中設(shè)置該指令的返回值
  • 調(diào)試者手動(dòng)利用調(diào)試器設(shè)置該值
  • 設(shè)置RPC地址,從區(qū)塊鏈中獲取該值

文章的開(kāi)頭提過(guò)了對(duì)我編寫(xiě)的調(diào)試器的定位問(wèn)題,也正是因?yàn)橛龅皆擃惖闹噶睿湃ニ伎颊{(diào)試器的定位。既然已經(jīng)打包進(jìn)了區(qū)塊,說(shuō)明是有交易地址的,既然有交易地址,那完全可以使用Remix的調(diào)試器進(jìn)行調(diào)試。

所以對(duì)我編寫(xiě)的調(diào)試器有了離線調(diào)試器的定位,采用上述方法中的前三個(gè)方法,優(yōu)先級(jí)由高到低分別是,手動(dòng)設(shè)置>配置文件設(shè)置>默認(rèn)設(shè)置

2. 獲取環(huán)境信息指令,比如:

ADDRESS: Get address of currently executing account.

獲取當(dāng)前合約的地址,解決方案如下:

  • 設(shè)置默認(rèn)值
  • 設(shè)置一個(gè)配置文件,在配置文件中設(shè)置該指令的返回值
  • 調(diào)試者手動(dòng)利用調(diào)試器設(shè)置該值

獲取環(huán)境信息的指令,因?yàn)檎{(diào)試的是OPCODE,沒(méi)有源碼,不需要部署,所以是沒(méi)法通過(guò)RPC獲取到的,只能由調(diào)試者手動(dòng)設(shè)置

3. 日志指令

LOG0-LOG4: Append log record with no topics.

把日志信息添加到交易的回執(zhí)單中

> eth.getTransactionReceipt("0xe32b3751a3016e6fa5644e59cd3b5072f33f27f10242c74980409b637dbb3bdc")
{
  blockHash: "0x04b838576b0c3e44ece7279b3b709e336a58be5786a83a6cf27b4173ce317ad3",
  blockNumber: 6068600,
  contractAddress: null,
  cumulativeGasUsed: 7171992,
  from: "0x915d631d71efb2b20ad1773728f12f76eeeeee23",
  gasUsed: 81100,
  logs: [],
  logsBloom: "0x00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000",
  status: "0x1",
  to: "0xd1ceeeefa68a6af0a5f6046132d986066c7f9426",
  transactionHash: "0xe32b3751a3016e6fa5644e59cd3b5072f33f27f10242c74980409b637dbb3bdc",
  transactionIndex: 150
}

上述就是獲取一個(gè)交易的回執(zhí)單,其中有一個(gè)logs列表,就是用來(lái)儲(chǔ)存日志信息

既然是在調(diào)試OPCODE,那么記錄日志的操作就是沒(méi)有必要的,因?yàn)檎{(diào)試的過(guò)程中能看到儲(chǔ)存器/參數(shù)的情況,所以對(duì)于這類指令的操作,完全可以直接輸出,或者不做任何處理(直接pass)

4. 系統(tǒng)操作指令

這類指令主要是外部調(diào)用相關(guān)的指令,比如可以創(chuàng)建合約的CREATE, 比如能調(diào)用其他合約的CALL, 比如銷毀自身,并把余額全部轉(zhuǎn)給別人的SELFDESTRUCT

這類的指令我認(rèn)為的解決辦法只有: 調(diào)試者手動(dòng)利用調(diào)試器設(shè)置該指令的返回值

調(diào)用這類函數(shù)的時(shí)候,我們完全能看到詳細(xì)的參數(shù)值,所以完全可以手動(dòng)的進(jìn)行創(chuàng)建合約,調(diào)用合約等操作。

總結(jié)

在完成一個(gè)OPCODE的解釋器后,一個(gè)調(diào)試器就算完成了3/4, 剩下的工作就是實(shí)現(xiàn)自己想實(shí)現(xiàn)的調(diào)試器功能,比如下斷點(diǎn),查看棧內(nèi)存儲(chǔ)存數(shù)據(jù)等

原文鏈接:https://paper.seebug.org/693/

上一篇:逆向 Bushido IOT 僵尸網(wǎng)絡(luò)

下一篇:CISO如何通過(guò)安全路線圖說(shuō)服高管