作者:安比(SECBIT)实验室 & 轻信科技(LedgerGo)
本文以蜜罐合约和 BancorLender 合约为例,详细介绍 Solidity 语言中「未初始化的 storage 指针」问题,并追踪 Solidity 编译器关于此问题的开发进展。
安比(SECBIT)实验室在 BancorLender (0x2d820ea3A6b9302c500feeb7F6361bA1DdfA5aBa) 合约中发现野指针问题(uninitialized-wild-pointer)。该合约中的一个状态变量会意外地被另一个函数修改,偏离原本设计意图。目前项目方不明确。建议项目方应立即废弃该合约,并重新发布修复后的合约。野指针问题是 Solidity 语言的最初设计欠缺考虑,而且 Solidity 编译器为了向前兼容,对这类安全问题仅采取警告提示,而开发者往往又很容易忽视这些提示,最终导致问题代码部署上线。
下面我们通过一个蜜罐例子来解释「未初始化的 storage 指针」这个缺陷。
蜜罐合约:别人看中的是你的本金
在计算机领域,蜜罐(Honeypot)通常指故意伪装成看似有利用价值并故意留有 bug 的系统,用来吸引黑客攻击,从而达到分析、监控、收集证据、拖延攻击等目的。
而以太坊主网上存在这样一类游戏合约:以高额回报为诱饵,并故意露出破绽,让参与者误认为自己有很高的概率可以获胜,诱导参与者转入以太参与游戏而损失本金。通常称这类合约为“蜜罐合约”。
“蜜罐”这个词,其实很形象:罐子里有可口的蜂蜜,吸引着熊去吃,但周边其实有暗藏的陷阱,真正目的是为了抓住熊。
“蜜罐合约”的部署者通常利用各种技巧使代码部分特殊用途不易被参与者发现,利用当中的信息不对称,使参与者产生错误判断,从而被骗取本金。
「未初始化的 storage 指针」正是“蜜罐合约”部署者最常用的一种技巧。这个问题源于 Solidity 语言以及编译器设计上的失误。
我们结合下面这个名为 Honeypot 的简化合约说明。这是一个竞猜合约,参与者调用 guess()
接口,传入 _number
数字进行竞猜,如果猜的数字等于合约中的 luckyNum
,则竞猜成功,参与者可获取两倍回报。
聪明的你可以仔细思考一下,竞猜数字 _number
应该填多少?
终极答案是 42 吗?由于变量 luckyNum
在最开始(第 2 行)被赋值 42,并且没有其他被赋值操作,因此绝大多数人都会猜 42。
然而这个合约极具迷惑性,42 并不是正确答案。到底哪里出了问题?变量 luckyNum
什么时候被修改了?
让我们来理一理:函数 guess()
先把参与者的地址和竞猜数字放入 gameHistory 数组中保存(第 12 ~ 15 行)。而数组 gameHistory
由 Game
结构体(Struct)构成。函数开始先通过 Game game
声明了一个结构体变量 game(第 12 行)
,再分别对成员变量进行赋值(第 13 ~ 14 行),最后将变量 game
塞到 gameHistory
数组中(第 15 行)。
看着“似乎”没毛病。然而,这里有很严重的问题。
传统编程语言中,我们在函数内部申明一个变量,通常默认是局部变量。但 Solidity 在语言设计上埋了个坑,在此处反直觉地默认让引用类型(Reference Type)变量 game
(第 12 行)存储位置为 storage,因此对变量 game
的修改,作用范围是“全局”的。并且对于未初始化的 storage 指针(类似传统语言中的空指针),Solidity 默认其指向 storage 的起始地址,即指向合约开头定义的状态变量(第 2 ~ 3 行)。
变量 luckyNum
值不是 42,那么到底是多少呢?
Solidity 将源码中的状态变量(常量除外),根据一定规则,按照出现顺序依次排列存储在 storage 中。
而 luckyNum<