天价手续费分析:我不是真土豪
谁造成了天价手续费?
`By:Thinking@慢雾安全团队` ## 事件背景 分析源自一笔转账金额 10w USDT,手续费却高达 7,676 枚 ETH 的天价手续费交易。 *(https://etherscan.io/tx/0x2c9931793876db33b1a9aad123ad4921dfb9cd5e59dbb78ce78f277759587115)* ![6a04a5c59885b283397afaa9f3dd0efd.png](https://img.learnblockchain.cn/attachments/2021/12/0lPpZRqU61b2f68a1eb2c.png) ## 关键代码分析 ### **根据此 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/src/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; } ``` **2. 判断是否可以被 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 的函数中进行处理。 ![8a6b4caf9a0f0c45864e0b9efefd169e.png](https://img.learnblockchain.cn/attachments/2021/12/HjG6L4HE61b2f9884dc60.png) 同步分析 intToBuffer 的处理过程,这部分和“关键代码分析”部分的代码逻辑是一样的,处理转换部分得到的结果是`7e9059bbe.8ccd`。 ![511e8bedf345af38029f8ae4e8d90350.png](https://img.learnblockchain.cn/attachments/2021/12/uq8lF2cq61b2f99b872c6.png) 接下来分析如何将转换后的字符填充进入的 buffer 中,通过这步可以得到 buffer 的内容是`126, 144, 89, 187, 14, 140, 205` 对应的是 `7e, 90, 59, bb, e, 8c, cd`。 ![0a0ea3b0f0beb95c29c4a4568613f901.png](https://img.learnblockchain.cn/attachments/2021/12/mbnJ3Eb861b2f9b2a0c2b.png) ``` > 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`。 ![38e926e12133212078b38b3b83c898c8.png](https://img.learnblockchain.cn/attachments/2021/12/yi4VKde961b2fa1020e8e.png) ### 处理方式分析:nodejs 由于浏览器上出问题的是`7e9059bbe.8ccd`在写入 buffer 的时候小数点被`parseInt`吃掉了导致数据出错,但是经过分析,node 的数据也是错误的,且产生错误的原因是和浏览器的不一样。 **首先我们先看下如下的示例:** node 三组不同的数据填充到 buffer 得到的结果居然是一样的,经过分析 node 的 buffer 有个小特性,就是 2 个一组切分后的数据,如果没法正常通过 hex 解析的,就会把那一组数据以及之后的数据都不处理了,直接返回前面可以被正常处理的那部分数据。可以理解为被截断了。这部分可以参考 node 底层的 buffer 中`node_buffer.cc`中的代码逻辑。 ``` > new Buffer('7e9059bbe', 'hex') <Buffer 7e 90 59 bb> > new Buffer('7e9059bbe.8ccd', 'hex')<Buffer 7e 90 59 bb> > new Buffer('7e9059bb', 'hex') <Buffer 7e 90 59 bb> ``` #### 执行结果的比较 node 由于会将原始数据 `7e9059bbe.8ccd` 中的`e.`及之后的数据进行截断,所以最终错误的值是 `7e9059bb`,相比正确的值 `07e9059bbe` 小。 **node 的执行结果:** ![2133f3292b09fd51c3f38f156a4eab67.png](https://img.learnblockchain.cn/attachments/2021/12/Wo6SmpXw61b2fb28907d9.png) 浏览器由于会将原始数据 `7e9059bbe.8ccd` 中的`.` 吃掉,所以最终错误的值是 `7e9059bbe8ccd`,相比正确的值 `07e9059bbe` 大很多。 **浏览器的执行结果:** ![fb30927a3e375fa7a4893f1613751d8f.png](https://img.learnblockchain.cn/attachments/2021/12/acMOgNM261b2fb48e8e73.png) ## **问题的原因** `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* 本文首发于:https://mp.weixin.qq.com/s/PN7UwkRA4jaxkK-93xPlHw
By:Thinking@慢雾安全团队
事件背景
分析源自一笔转账金额 10w USDT,手续费却高达 7,676 枚 ETH 的天价手续费交易。 (https://etherscan.io/tx/0x2c9931793876db33b1a9aad123ad4921dfb9cd5e59dbb78ce78f277759587115)
关键代码分析
根据此 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/src/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;
}
2. 判断是否可以被 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
由于浏览器上出问题的是7e9059bbe.8ccd
在写入 buffer 的时候小数点被parseInt
吃掉了导致数据出错,但是经过分析,node 的数据也是错误的,且产生错误的原因是和浏览器的不一样。
首先我们先看下如下的示例:
node 三组不同的数据填充到 buffer 得到的结果居然是一样的,经过分析 node 的 buffer 有个小特性,就是 2 个一组切分后的数据,如果没法正常通过 hex 解析的,就会把那一组数据以及之后的数据都不处理了,直接返回前面可以被正常处理的那部分数据。可以理解为被截断了。这部分可以参考 node 底层的 buffer 中node_buffer.cc
中的代码逻辑。
> new Buffer('7e9059bbe', 'hex')
<Buffer 7e 90 59 bb>
> new Buffer('7e9059bbe.8ccd', 'hex')<Buffer 7e 90 59 bb>
> new Buffer('7e9059bb', 'hex')
<Buffer 7e 90 59 bb>
执行结果的比较
node 由于会将原始数据 7e9059bbe.8ccd
中的e.
及之后的数据进行截断,所以最终错误的值是 7e9059bb
,相比正确的值 07e9059bbe
小。 node 的执行结果:
浏览器由于会将原始数据 7e9059bbe.8ccd
中的.
吃掉,所以最终错误的值是 7e9059bbe8ccd
,相比正确的值 07e9059bbe
大很多。
浏览器的执行结果:
问题的原因
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
本文首发于:https://mp.weixin.qq.com/s/PN7UwkRA4jaxkK-93xPlHw
- 发表于 2021-12-10 15:06
- 阅读 ( 189 )
- 学分 ( 1 )
- 分类:资讯
评论