作者:Sissel@知道創宇404區塊鏈安全研究團隊
時間:2018年8月20日
英文版:http://www.bjnorthway.com/687/

0x00 前言

2018年08月01日,知道創宇404區塊鏈安全研究團隊發布《金錢難寐,大盜獨行——以太坊 JSON-RPC 接口多種盜幣手法大揭秘》,針對 偷渡漏洞后偷渡時代的盜幣方式 進行了介紹,披露了 后偷渡時代 的三種盜幣方式:離線攻擊、重放攻擊和爆破攻擊。

在進一步的研究中,我們又發現了針對這些攻擊方式的補充:拾荒攻擊。攻擊者或求助于礦工,或本身擁有一定算力以獲得將交易打包進區塊的權利。在偷渡漏洞中,攻擊者在被攻擊節點構造gasPrice0 的交易,等待用戶解鎖賬戶簽名廣播。攻擊者同時設置一個惡意節點,用于接收這筆交易。攻擊者將符合條件的交易打包,就可以實現 0 手續費完成轉賬。通過這種攻擊,攻擊者可以獲取到余額不足以支付轉賬手續費或勉強足夠支付手續費節點上的所有以太幣,并在一定程度上可以防止其他攻擊者的競爭,可謂是 薅羊毛 的典范。

除此之外,在薅夠以太幣殘羹之后,攻擊者又盯上了這些以太幣已被盜光,但賬戶中殘留的代幣。直到現在,針對許多智能合約發行的代幣,一些被攻擊賬戶中的token,仍在小額地被攻擊者以拾荒攻擊盜走。

本文將從一筆零手續費交易談起,模擬復現盜幣的實際流程,對拾荒攻擊成功的關鍵點進行分析。

0x01 從一筆零手續費交易談起

在區塊鏈系統中,每一筆交易都應該附帶一部分gas以及相應的gasPrice作為手續費,當該交易被打包進區塊,這筆手續費將用來獎勵完成打包的礦工。

《金錢難寐,大盜獨行——以太坊 JSON-RPC 接口多種盜幣手法大揭秘》中,我們提到了一個利用以太坊JSON-RPC接口的攻擊者賬號0x957cD4Ff9b3894FC78b5134A8DC72b032fFbC464。該攻擊者在公網中掃描開放的RPC端口,構造高手續費的交易請求,一旦用戶解鎖賬戶,便會將用戶余額轉至攻擊者的賬戶或攻擊者創建的合約賬戶。

在分析該賬戶交易信息的時候,我們發現了一筆不符合常識的交易,先從這筆交易開始談起。

交易地址:0xb1050b324f02e9a0112e0ec052b57013c16156301fa7c894ebf2f80ac351ac22

Function: transfer(address _to, uint256 _value)

MethodID: 0xa9059cbb
[0]:  000000000000000000000000957cd4ff9b3894fc78b5134a8dc72b032ffbc464
[1]:  000000000000000000000000000000000000000000000000000000000abe7d00

0x00a329c0648769a73afac7f9381e08fb43dbea72向合約MinereumToken(攻擊者的合約)的交易,雖然用戶余額很少,但這筆交易使用了該賬戶所有余額作為value與合約交互,這筆交易使用了正常數量的gas,但它的gasPrice被設定為0。

前文提到,攻擊者會使用較高的手續費來保證自己的交易成功,礦工會按照本節點的txpool中各交易的gasPrice倒序排列,優先將高gasPrice交易打包進之后的區塊。在這個世界上每時每刻都在發生著無數筆交易,在最近七日,成交一筆交易的最低gasPrice是3Gwei。這筆零手續費交易究竟是如何發生,又是如何打包進區塊的呢。

0x02 思路分析

在區塊鏈系統中,任何人都可以加入區塊鏈網絡,成為其中一個節點,參與記賬、挖礦等操作。保證區塊鏈的可信性和去中心化的核心便是共識機制

共識機制

在以太坊中,礦工將上一區塊的哈希值、txpool中手續費較高的交易、時間戳等數據打包,不斷計算nonce來挖礦,最先得出符合條件的nonce值的礦工將擁有記賬權,得到手續費和挖礦獎勵。礦工將廣播得到的區塊,其他節點會校驗這一區塊,若無錯誤,則認為新的區塊產生,區塊鏈高度增加。這就是各節點生成新區塊保持共識的過程。

將0 gasPrice交易完成需要確認兩個問題

  • 礦工是否會接受這個交易,并將其打包
  • 其余節點接收到含此交易的區塊,是否會達成共識

下面我們來對0 gasPrice交易相關的操作進行測試。了解零手續費的交易如何產生,如何被txpool接受,打包了零手續費交易的區塊能否被認可,確認上述問題的答案。

0x03 零手續費交易測試

a. 單節點測試

首先,我們來確認此交易是否可以進入節點的txpool中,啟用一個測試鏈。默認rpc端口是8545,使用python的web3包發起一筆0 gasPrice轉賬。

geth --networkid 233 --nodiscover --verbosity 6 --ipcdisable --datadir data0 --rpc --rpcaddr 0.0.0.0 console

節點一發起轉賬的腳本,轉帳前要解鎖賬戶

from web3 import Web3, HTTPProvider
web3 = Web3(HTTPProvider("http://localhost:8545/"))

print(web3.eth.accounts)
# 轉帳前要解鎖賬戶
web3.eth.sendTransaction({
    "from":web3.eth.accounts[0],
    "to":web3.eth.accounts[1],
    "value": 10,
    "gas":21000,
    "gasPrice":0,
    })

交互結果

> txpool.content
{
  pending: {},
  queued: {}
}
> eth.getBalance(eth.accounts[0])
800000000
> personal.unlockAccount(eth.accounts[0],'sissel')
true
> INFO [08-14|11:20:14.972] Submitted transaction                    fullhash=0x72e81751d2517807cabad24102d3cc2f0f4f2e8b92f1f106f1ee0bf6be734fe4 recipient=0x92636b228148e2824cB8d472Ef2F4e76f2F5059C
> txpool.content
{
  pending: {
    0x092fda221a114FA702e2f59C217C92cfEB63f5AC: {
      3: {
        blockHash: "0x0000000000000000000000000000000000000000000000000000000000000000",
        blockNumber: null,
        from: "0x092fda221a114fa702e2f59c217c92cfeb63f5ac",
        gas: "0x5208",
        gasPrice: "0x0",
        hash: "0x72e81751d2517807cabad24102d3cc2f0f4f2e8b92f1f106f1ee0bf6be734fe4",
        input: "0x",
        nonce: "0x3",
        r: "0x1eca20e3f371ed387b35ca7d3220789399a3f64c449a825e0fa7423b96ce235c",
        s: "0x35a58e5cb5027c7903c1f1cc061ae846fb5150186ebbabb2b0766e4cbfc4aee6",
        to: "0x92636b228148e2824cb8d472ef2f4e76f2f5059c",
        transactionIndex: "0x0",
        v: "0x42",
        value: "0xa"
      }
    }
  },
  queued: {}
}
> miner.start(1)
INFO [08-14|11:20:35.715] Updated mining threads                   threads=1
INFO [08-14|11:20:35.716] Transaction pool price threshold updated price=18000000000
null
INFO [08-14|11:20:35.717] Starting mining operation
> INFO [08-14|11:20:35.719] Commit new mining work                   number=115 txs=1 uncles=0 elapsed=223μs
> mINFO [08-14|11:20:36.883] Successfully sealed new block            number=115 hash=ce2f34…210039
INFO [08-14|11:20:36.885] ? block reached canonical chain          number=110 hash=2b9417…850c25
INFO [08-14|11:20:36.886] ? mined potential block                  number=115 hash=ce2f34…210039
INFO [08-14|11:20:36.885] Commit new mining work                   number=116 txs=0 uncles=0 elapsed=202μs
> miner.stop()
true
> eth.getBalance(eth.accounts[0])
799999990

節點一發起的零手續費交易成功,并且挖礦后成功將該交易打包進區塊中。

b. 多節點共識測試

現在加入另一個節點

geth  --datadir "./" --networkid 233 --rpc  --rpcaddr "localhost" --port 30304 --rpcport "8546" --rpcapi "db,eth,net,web3" --verbosity 6 --nodiscover console
使用這些方法添加節點
> admin.nodeInfo
> admin.addPeer()
> admin.peers

節點一仍使用剛才的腳本發起零手續費交易,節點一的txpool中成功添加,但節點二因為gasPrice非法拒絕了此交易。

TRACE[08-15|10:09:24.682] Discarding invalid transaction           hash=3902af…49da03 err="transaction underpriced"

> txpool.content
 []

在geth的配置中發現了與此相關的參數

--txpool.pricelimit value    Minimum gas price limit to enforce for acceptance into the pool (default: 1)

將其啟動時改為0,但節點二的txpool中仍未出現這筆交易。

閱讀源碼知,此參數確實是控制txpool增加的交易的最低gasPrice,但不能小于1。

if conf.PriceLimit < 1 {
    log.Warn("Sanitizing invalid txpool price limit", "provided", conf.PriceLimit, "updated", DefaultTxPoolConfig.PriceLimit)
    conf.PriceLimit = DefaultTxPoolConfig.PriceLimit
}

令節點一(txpool中含0 gasPrice)開始挖礦,將該交易打包進區塊后,發現節點二認可了此區塊,達成共識,兩節點高度均增長了。

得到結論:

  • 零手續費交易,通常情況下只有發起者的txpool可以接收,其余節點無法通過同步此交易。如若需要,必須進行修改geth源碼等操作。
  • 雖然這筆交易無法進入其他節點的txpool,但對于含此交易的區塊,可以達成共識。

我們將進行簡要的源代碼分析,支持我們的結論。

0x04 源碼分析

(以下的代碼分析基于https://github.com/ethereum/go-ethereum的當前最新提交:commit 6d1e292eefa70b5cb76cd03ff61fc6c4550d7c36)

以太坊目前最流行的節點程序(Geth/Parity)都提供了RPC API,用于對接礦池、錢包等其他第三方程序。首先確認一下節點在打包txs時,代碼的實現。

i. 交易池

代碼路徑:./go-ethereum/core/tx_pool.go

// TxPool contains all currently known transactions. Transactions
// enter the pool when they are received from the network or submitted
// locally. They exit the pool when they are included in the blockchain.
type TxPool struct {
    config       TxPoolConfig
    chainconfig  *params.ChainConfig
    chain        blockChain
    gasPrice     *big.Int             //最低的GasPrice限制
    /*
       其他參數
    */
}

生成一個tx實例時,發現有對gasPrice的最低要求,具體在這個函數中會拒絕接收此交易。

// validateTx checks whether a transaction is valid according to the consensus
// rules and adheres to some heuristic limits of the local node (price and size).
func (pool *TxPool) validateTx(tx *types.Transaction, local bool) error {

    // 在這里是gasPrice的校驗
    if !local && pool.gasPrice.Cmp(tx.GasPrice()) > 0 {
        return ErrUnderpriced
    }

    /*
       ...
    */

    return nil
}

ii. 移除低于閾值的交易

代碼路徑:./go-ethereum/core/tx_list.go 并且在處理txs中,會將低于閾值的交易刪除,但本地的交易不會刪除。

// Cap finds all the transactions below the given price threshold, drops them
// from the priced list and returs them for further removal from the entire pool.
func (l *txPricedList) Cap(threshold *big.Int, local *accountSet) types.Transactions {
    drop := make(types.Transactions, 0, 128) // Remote underpriced transactions to drop
    save := make(types.Transactions, 0, 64)  // Local underpriced transactions to keep

    for len(*l.items) > 0 {
        // Discard stale transactions if found during cleanup
        tx := heap.Pop(l.items).(*types.Transaction)
        if _, ok := (*l.all)[tx.Hash()]; !ok {
            // 如果發現一個已經刪除的,那么更新states計數器
            l.stales--
            continue
        }
        // Stop the discards if we've reached the threshold
        if tx.GasPrice().Cmp(threshold) >= 0 {
            // 如果價格不小于閾值, 那么退出
            save = append(save, tx)
            break
        }
        // Non stale transaction found, discard unless local
        if local.containsTx(tx) {  //本地的交易不會刪除
            save = append(save, tx)
        } else {
            drop = append(drop, tx)
        }
    }
    for _, tx := range save {
        heap.Push(l.items, tx)
    }
    return drop
}

以上部分為區塊鏈網絡內一節點,嘗試接收或加入 0 gasPrice 的交易時,會有部分過濾或規則限制。但通過修改源碼,我們依然可以做到將 0 gasPrice 的交易合法加入到區塊中,并進行之后的nonce計算。下面繼續源碼分析,考察通過此方式得到的區塊,是否可以被其他節點接受,達成共識。

iii. 共識校驗

代碼路徑:./go-ethereum/consensus/consensus.go 這是geth中,提供的共識算法engine接口

type Engine interface {
    // 簽名
    Author(header *types.Header) (common.Address, error)

    /*
        驗證了header、seal,處理難度等函數
       ...
    */

    // 預處理區塊頭信息,修改難度等
    Prepare(chain ChainReader, header *types.Header) error

    // 區塊獎勵等,挖掘出區塊后的事情
    Finalize(chain ChainReader, header *types.Header, state *state.StateDB, txs []*types.Transaction,
        uncles []*types.Header, receipts []*types.Receipt) (*types.Block, error)

    // 計算nonce,若收到更高的鏈,則退出
    Seal(chain ChainReader, block *types.Block, stop <-chan struct{}) (*types.Block, error)

    // 計算難度值
    CalcDifficulty(chain ChainReader, time uint64, parent *types.Header) *big.Int

    // APIs returns the RPC APIs this consensus engine provides.
    APIs(chain ChainReader) []rpc.API

    // Close terminates any background threads maintained by the consensus engine.
    Close() error
}

查看VerifySeal(),發現校驗了如下內容:

  • 不同模式下的一些特殊處理
  • 難度是否合法
  • nonce值是否合法
  • gas值是否合法

可以看到,其他節點針對共識,檢查了簽名、nonce等內容,對于其中零手續費的交易沒有檢驗。換句話說,零手續費的交易雖然不能激勵礦工,但它依然是合法的。

0x05 利用流程

攻擊者首先以偷渡漏洞利用的方式,構造零手續費,正常的transfer交易。待用戶解鎖賬戶后,廣播交易。具體流程見下圖:

0x06 小結

由此我們可以得出,0 gasPrice這樣的特殊交易,有如下結論:

  • 通常情況下,0 gasPrice可通過節點自身發起加入至txpool中。
  • 以 geth 為例,修改geth部分源碼重新編譯運行,該節點方可接受其他節點發出的特殊交易(目標賬戶發起的0 gasPrice交易)。此為攻擊者需要做的事情。
  • 0 gasPrice的交易可以打包進區塊,并且符合共識要求。

因為json-rpc接口的攻擊方式中,攻擊者可以通過偷渡漏洞簽名 0 gasPrice交易并廣播。通過收集此類0 gasPrice交易并添加至部分礦工的txpool中,當該礦工挖出一個新的區塊,這類交易也將會被打包。即攻擊者可能與部分礦工聯手,或攻擊者本身就有一定的運算能力,讓礦工不再遵循誠實挖礦維護區塊鏈系統的原則,

0x07 利用價值及防御方案

因為零手續費交易的出現,諸多低收益的攻擊都將擁有意義。

提高收益

攻擊者可以通過此種方式,結合其他的攻擊手法,將被攻擊賬戶中的余額全部轉出,達到了收益最大化。

羊毛薅盡

依照《金錢難寐,大盜獨行——以太坊 JSON-RPC 接口多種盜幣手法大揭秘》中提到的攻擊方式,對于賬戶余額較少,甚至不足以支付轉賬手續費的情況,可通過上文提到的薅羊毛式攻擊方案,將賬戶中的殘羹收入囊中。由于此交易gasPrice為0,可在一區塊中同時打包多個此類型交易,例如此合約下的多組交易:0x1a95b271b0535d15fa49932daba31ba612b52946,此區塊中的幾筆交易:4788940

偷渡代幣

在被盜賬戶已無以太幣的情況下,攻擊者發現這些賬戶還存有部分智能合約發行的代幣。沒有以太幣便不能支付gas進行轉賬,零手續費交易可以完美解決這個問題。直到現在,有諸多無以太幣的被攻擊賬戶,仍在被此方式轉賬代幣。

防御方案

由于0 gasPrice交易只是擴展其他攻擊方案的手法,還應將防御著眼在之前json-rpc接口利用。

  • 對于有被偷渡漏洞攻擊的痕跡或可能曾經被偷渡漏洞攻擊過的節點,建議將節點上相關賬戶的資產轉移到新的賬戶后廢棄可能被攻擊過的賬戶。
  • 建議用戶不要使用弱口令作為賬戶密碼,如果已經使用了弱口令,可以根據1.2節末尾的內容解出私鑰內容,再次通過 geth account import 命令導入私鑰并設置強密碼。
  • 如節點不需要簽名轉賬等操作,建議節點上不要存在私鑰文件。如果需要使用轉賬操作,務必使用 personal_sendTransaction 接口,而非 personal_unlockAccount 接口。

0x08 影響規模

我們從上面說到的0 gasPrice的交易入手。調查發現,近期依然有許多交易,以0 gasPrice成交。多數0手續費交易都出自礦池:0xb75d1e62b10e4ba91315c4aa3facc536f8a922f50x52e44f279f4203dcf680395379e5f9990a69f13c,例如區塊 61612146160889等。

我們注意到,這些0 gasPrice交易,僅有早期的少部分交易,會攜帶較少的以太幣,這符合我們對其薅羊毛特性的預計。經統計,從2017年6月起,陸續有748個賬戶總計24.2eth被零手續費轉賬。

在其中也找到了《金錢難寐,大盜獨行——以太坊 JSON-RPC 接口多種盜幣手法大揭秘》中提到的重放攻擊,造成的賬戶損失:0x682bd7426ab7c7b4b5beed331d5f82e1cf2cecc83c317ccee6b4c4f1ae34d909

被盜走0.05eth


在這些0 gasPrice中,更多的是對合約發行的TOKEN,進行的轉賬請求,將用戶賬戶中的token轉移至合約擁有者賬戶中,例如:

該賬戶的tx記錄。

攻擊者擁有多個礦池的算力,將眾多被攻擊賬戶擁有的多種token,轉移到相應的賬戶中,雖然單筆交易金額較小,但可進行此種攻擊方式的賬戶較多,合約較多,且不需要手續費。積少成多,直到現在,攻擊者仍在對這些代幣進行著拾荒攻擊。

0x09 結語

區塊鏈系統基于去中心化能達成交易的共識,一個前提就是,絕大多數的礦工,都會通過誠實挖礦來維持整個比特幣系統。當礦工不再誠實,區塊鏈的可信性和去中心化將會大打折扣。當黑客聯合礦工,或黑客本身擁有了算力成為礦工,都會在現有攻擊手法的基礎上,提供更多的擴展攻擊方案。0 gasPrice交易的出現,違背了區塊鏈設計初衷,即應對礦工支付手續費作為激勵。 區塊鏈技術與虛擬貨幣的火熱,賦予了鏈上貨幣們巨大的經濟價值,每個人都想在區塊鏈浪潮中分得一杯羹。黑客們更是如此,他們作為盜幣者,絞盡腦汁的想著各個角度攻擊區塊鏈與合約。當黑客棲身于礦工,他們不但能挖出區塊,也能挖出漏洞。


智能合約審計服務

針對目前主流的以太坊應用,知道創宇提供專業權威的智能合約審計服務,規避因合約安全問題導致的財產損失,為各類以太坊應用安全保駕護航。

知道創宇404智能合約安全審計團隊: https://www.scanv.com/lca/index.html
聯系電話:(086) 136 8133 5016(沈經理,工作日:10:00-18:00)

歡迎掃碼咨詢:

區塊鏈行業安全解決方案

黑客通過DDoS攻擊、CC攻擊、系統漏洞、代碼漏洞、業務流程漏洞、API-Key漏洞等進行攻擊和入侵,給區塊鏈項目的管理運營團隊及用戶造成巨大的經濟損失。知道創宇十余年安全經驗,憑借多重防護+云端大數據技術,為區塊鏈應用提供專屬安全解決方案。

歡迎掃碼咨詢:


參考鏈接

  1. json-rpc接口盜幣手法:金錢難寐,大盜獨行——以太坊 JSON-RPC 接口多種盜幣手法大揭秘
  2. https://www.reddit.com/r/ethereum/comments/7lx1do/a_christmas_mystery_sweepers_and_zero_gas_price/
  3. how-to-create-your-own-private-ethereum-blockchain-dad6af82fc9f
  4. 零手續費交易:https://etherscan.io/tx/0xb1050b324f02e9a0112e0ec052b57013c16156301fa7c894ebf2f80ac351ac22
  5. 慢霧命名的“以太坊黑色情人節”,細節:以太坊生態缺陷導致的一起億級代幣盜竊大案:https://mp.weixin.qq.com/s/Kk2lsoQ1679Gda56Ec-zJg
  6. 揭秘以太坊中潛伏多年的“偷渡”漏洞,全球黑客正在瘋狂偷幣:http://www.bjnorthway.com/547/

Paper 本文由 Seebug Paper 發布,如需轉載請注明來源。本文地址:http://www.bjnorthway.com/673/