深度解析 Bitfinex 天價手續費轉賬:BUG+顯示錯誤「釀成大錯」

撰文:Thinking @ 慢霧安全團隊

事件背景

分析源自一筆轉賬金額 10w USDT,手續費卻高達 7,676 枚 ETH 的天價手續費交易,

https://etherscan.io/tx/0x2c9931793876db33b1a9aad123ad4921dfb9cd5e59dbb78ce78f277759587115

核心點

技術層面的核心問題是: ethjs-util 的 intToBuffer 不支持傳入浮點型的數據,ethereumjs 用了 ethjs-util 的 intToBuffer ,

簡而言之:DApp 在使用 ethereumjs 構造交易的時候,傳入的手續費如果帶有小數的,會因為類型轉換出現 bug 導致在瀏覽器返回了一個很大的數作為手續費。且硬件錢包沒有顯示清楚,導致用戶直接授權簽名了這邊天價手續費的交易,

關鍵代碼分析

根據此 Issue

https://github.com/ethereumjs/ethereumjs-monorepo/issues/1497 中的描述,開始了分析,

我們以倒敘的方式來說明問題,這樣更方便理解。核心問題是 ethjs-util 的 intToBuffer 不支持傳入浮點型的數據,

首先看看關鍵的代碼,提得比較多的是 ethereumjs 的問題,主要聚焦討論的是 maxPriorityFeePerGas 和 maxFeePerGas 這兩個參數的值,由于傳入的浮點型,導致計算錯誤,得到了錯的手續,從而發生了 「天價手續的事件」,

經過分析后,這兩個參數都是經過 toBuffer 進行處理的,所以開始分析 toBuffer。

https://github.com/ethereumjs/ethereumjs-monorepo/blob/cf95e04c6a/packages/tx/hide/eip1559Transaction.ts#L200-L201

this.maxFeePerGas = new BN(toBuffer(maxFeePerGas === ” ? ‘0x’ : maxFeePerGas)) this.maxPriorityFeePerGas = new BN( toBuffer(maxPriorityFeePerGas === ” ? ‘0x’ : maxPriorityFeePerGas) )

toBuffer 會去調用 ethjs-util 的 intToBuffer 函數,這個函數主要處理了兩件事情,

https://github.com/ethjs/ethjs-util/blob/e9aede6681/dist/ethjs-util.js#L1950

function intToBuffer(i) { var hex = intToHex(i); return new Buffer(padToEven(hex.slice(2)), ‘hex’);}

  1. 將 int 轉成 Hex

https://github.com/ethjs/ethjs-util/blob/e9aede6681/dist/ethjs-util.js#L1939

function intToHex(i) { var hex = i.toString(16); // eslint-disable-line return ‘0x’ + hex;}

  1. 判斷是否可以被 2 整除,如果不行需要在字符開頭添加一個 0 ,這里主要是為了能夠成功的將數據 2 個 1 組寫入到 buffer。

https://github.com/ethjs/ethjs-util/blob/e9aede6681/dist/ethjs-util.js#L1920

function padToEven(value) { var a = value; // eslint-disable-line if (typeof a !== ‘string’) { throw new Error(‘[ethjs-util] while padding to even, value must be string, is currently ‘ + typeof a + ‘, while padToEven.’); } if (a.length % 2) { a = ‘0’ + a; } return a;}

以出錯的示例數據:33974229950.550003 進行分析,經過 intToBuffer 函數中的 intToHex 和 padToEven 處理后得到 7e9059bbe.8ccd,這部分瀏覽器 js 和 nodejs 的結果都是一致的,

不一致的地方是在 new Buffer 的操作:

new Buffer(padToEven(hex.slice(2)), ‘hex’);

處理方式分析:瀏覽器 js

通過 webpack 打包好 js 文件并對文件進行引用,然后在瀏覽器上進行調試分析。

首先輸入的示例字符 33974229950.550003 會進入到 intToBuffer 的函數中進行處理。

同步分析 intToBuffer 的處理過程,這部分和」關鍵代碼分析「部分的代碼邏輯是一樣的,處理轉換部分得到的結果是 7e9059bbe.8ccd,

接下來分析如何將轉換后的字符填充進入的 buffer 中,通過這步可以得到 buffer 的內容是 126, 144, 89, 187, 14, 140, 205 對應的是 7e, 90, 59, bb, e, 8c, cd。

> 0x7e -> 126> 0x90 -> 144> 0x59 -> 89> 0xbb -> 187> 0xe -> 14> 0x8c -> 140> 0xcd -> 205

這里發現 e. 這部分的小數點消失了,于是開始解小數點消失之迷,追蹤到 hexWrite 這個函數,這個函數會將得到的數據 2 個一組進行切分,然后用了 parseInt 對切分后的數據進行解析。

然而 parseInt(‘e.’,16) -> 14===parseInt(‘e’,16) -> 14 消失的小數點被 parseInt 吃掉了,導致最終寫入到 buffer 中的數據發生了錯誤,寫入 buffer 的值是 7e9059bbe8ccd,

處理方式分析:nodejs

由于瀏覽器上出問題的是 7_**__**_e9059bbe.8ccd 在寫入 buffer 的時候小數點被 parseInt 吃掉了導致數據出錯,但是經過分析,node 的數據也是錯誤的,且產生錯誤的原因是和瀏覽器的不一樣。

首先我們先看下如下的示例:

node 三組不同的數據填充到 buffer 得到的結果居然是一樣的,經過分析 node 的 buffer 有個小特性,就是 2 個一組切分后的數據,如果沒法正常通過 hex 解析的,就會把那一組數據以及之后的數據都不處理了,直接返回前面可以被正常處理的那部分數據。可以理解為被截斷了,這部分可以參考 node 底層的 buffer 中 node_buffer.cc 中的代碼邏輯,

> new Buffer(‘7e9059bbe’, ‘hex’) > new Buffer(‘7e9059bbe.8ccd’, ‘hex’) > new Buffer(‘7e9059bb’, ‘hex’)

執行結果的比較

node 由于會將原始數據 7e9059bbe.8ccd 中的 e. 及之后的數據進行截斷,所以最終錯誤的值是 7e9059bb,相比正確的值 07e9059bbe 小,

node 的執行結果:

瀏覽器由于會將原始數據 7e9059bbe.8ccd 中的 . 吃掉,所以最終錯誤的值是 7e9059bbe8ccd,相比正確的值 07e9059bbe 大很多。

瀏覽器的執行結果:

問題的原因

ethjs-util 的 intToBuffer 函數不支持浮點型的數據,且在這個函數中沒有判斷傳入的變量類型,來確保變量類型是預期內的,由于 ethereumjs 的 toBuffer 引用了 ethjs-util 的 intToBuffer 進行處理,也沒有對數據進行檢查。導致了這次事件的發生,所幸最終善良的礦工歸還了「天價手續費 7626 ETH」,

吸取的教訓

從第三方的庫的角度來看,在編碼過程中應該要遵循可靠的安全的編碼規范,在函數的開頭要對傳入的數據進行合法性的檢查,確保數據和代碼邏輯是按照預期內執行。

從庫的使用者的角度來看,使用者應該要自行閱讀第三方庫的開發文檔和對接文檔,并且也要對代碼中接入第三方庫的邏輯進行測試,通過構造大量的數據進行測試,確保業務上能夠正常按照期望執行,保證高標準的測試用例的覆蓋率,

0 条回复 A文章作者 M管理員
    暫無討論,說說你的看法吧