核心點:ethjs-util 的intToBuffer 不支持傳入浮點型的數據,ethereumjs 用了ethjs-util 的intToBuffer
撰文:Thinking @ 慢霧安全團隊
事件背景
分析源自一筆轉賬金額10w USDT,手續費卻高達7,676 枚ETH 的天價手續費交易。 https://etherscan.io/tx/0x2c9931793876db33b1a9aad123ad4921dfb9cd5e59dbb78ce78f277759587115慢霧深度解析Bitfinex 天價手續費轉賬:BUG+顯示錯誤「釀成大錯」
核心點
技術層面的核心問題是: 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’: maxPriorityFeePerG)
toBuffer 會去調用ethjs-util 的intToBuffer 函數,這個函數主要處理了兩件事情。
https://github.com/ethjs/ethjs-util/blob/e9aede6681/dist/ethjs-util.js#L1950
函數 intToBuffer(i) { var hex = intToHex(i); 返回新緩衝區(padToEven(hex.slice(2)), ‘hex’);}
- 將int 轉成Hex
https://github.com/ethjs/ethjs-util/blob/e9aede6681/dist/ethjs-util.js#L1939
函數 intToHex(i) { var hex = i.toString(16); // eslint-disable-line return ‘0x’ + hex;}
- 判斷是否可以被2 整除,如果不行需要在字符開頭添加一個0 ,這裡主要是為了能夠成功的將數據2 個1 組寫入到buffer。
https://github.com/ethjs/ethjs-util/blob/e9aede6681/dist/ethjs-util.js#L1920
函數 padToEven(value) { var a = value; // eslint-disable-line if (typeof a !== ‘string’) { throw new Error(‘[ethjs-util] 填充到偶數時,值必須是字符串,當前是 ‘ + typeof a + ‘,而 padToEven.’); } if (a.length % 2) { a = ‘0’ + a; } 返回一個;}
以出錯的示例數據:33974229950.550003 進行分析,經過intToBuffer 函數中的intToHex 和padToEven 處理後得到7e9059bbe.8ccd,這部分瀏覽器js 和nodejs 的結果都是一致的。
不一致的地方是在new Buffer 的操作:
新緩衝區(padToEven(hex.slice(2)),’十六進制’);
處理方式分析:瀏覽器js
通過webpack 打包好js 文件並對文件進行引用,然後在瀏覽器上進行調試分析。
首先輸入的示例字符33974229950.550003 會進入到intToBuffer 的函數中進行處理。慢霧深度解析Bitfinex 天價手續費轉賬:BUG+顯示錯誤「釀成大錯」同步分析intToBuffer 的處理過程,這部分和」關鍵代碼分析「部分的代碼邏輯是一樣的,處理轉換部分得到的結果是7e9059bbe.8ccd。慢霧深度解析Bitfinex 天價手續費轉賬:BUG+顯示錯誤「釀成大錯」接下來分析如何將轉換後的字符填充進入的buffer 中,通過這步可以得到buffer 的內容是126, 144, 89, 187, 14, 140, 205 對應的是7e, 90, 59, bb, e, 8c, cd。慢霧深度解析Bitfinex 天價手續費轉賬:BUG+顯示錯誤「釀成大錯」
> 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。慢霧深度解析Bitfinex 天價手續費轉賬:BUG+顯示錯誤「釀成大錯」
處理方式分析: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 的執行結果:慢霧深度解析Bitfinex 天價手續費轉賬:BUG+顯示錯誤「釀成大錯」瀏覽器由於會將原始數據7e9059bbe.8ccd 中的. 吃掉,所以最終錯誤的值是7e9059bbe8ccd,相比正確的值07e9059bbe 大很多。
瀏覽器的執行結果:慢霧深度解析Bitfinex 天價手續費轉賬:BUG+顯示錯誤「釀成大錯」
問題的原因
ethjs-util 的intToBuffer 函數不支持浮點型的數據,且在這個函數中沒有判斷傳入的變量類型,來確保變量類型是預期內的。由於ethereumjs 的toBuffer 引用了ethjs-util 的intToBuffer 進行處理,也沒有對數據進行檢查。導致了這次事件的發生,所幸最終善良的礦工歸還了「天價手續費7626 ETH」。
吸取的教訓
從第三方的庫的角度來看,在編碼過程中應該要遵循可靠的安全的編碼規範,在函數的開頭要對傳入的數據進行合法性的檢查,確保數據和代碼邏輯是按照預期內執行。
從庫的使用者的角度來看,使用者應該要自行閱讀第三方庫的開發文檔和對接文檔,並且也要對代碼中接入第三方庫的邏輯進行測試,通過構造大量的數據進行測試,確保業務上能夠正常按照期望執行,保證高標準的測試用例的覆蓋率。
參考資料 :
https://github.com/ethereumjs/ethereumjs-monorepo/issues/1497
https://blog.deversifi.com/23-7-million-dollar-ethereum-transaction-fee-post-mortem/
https://www.chainnews.com/news/611706276133.htm
展開全文打開碳鏈價值APP 查看更多精彩資訊