基于符号执行的代币买卖漏洞和权限转移漏洞的检测验证方法
2022-07-06刘宇航刘军杰文伟平
刘宇航 刘军杰 文伟平
(北京大学软件与微电子学院 北京 100871)
随着区块链技术和应用的不断发展落地,越来越多的用户和软件工程师倾向于在区块链平台上开发、交互应用甚至配置数字资产.智能合约就是存储和运行在区块链上的应用程序,通过智能合约和区块链的POW,POS,PBFT等共识机制,可以在数字世界建立信任.以太坊、币安智能链等很多区块链平台都支持智能合约的运行.区块链的智能合约一般由图灵完备的编程语言写成.智能合约带给开发者和用户以无限的创造性,能够使用智能合约编程语言(例如Solidity)定义各种规则,生成更多去中心化的应用供平台用户进行调用和交互.
这种去中心化、灵活可变、安全可靠的特性,使得智能合约被广泛地用于去中心化金融(DeFi[1])和数字货币领域.
然而由于智能合约开发者的代码不规范、智能合约编程语言或虚拟机固有的缺陷,使得区块链上存在大量有漏洞甚至容易被利用的智能合约,造成了很多智能合约攻击事件的发生,尤其是在去中心化金融领域,很多有漏洞的智能合约上存储的数字资产被攻击者一卷而空,产生不可估量的损失.
在DeFi领域中最常见的攻击事件中,一类是欺诈性代币攻击事件,也被称为“貔貅盘”事件.这种攻击一般和与去中心化交易所进行交互的代币合约有关,这种代币合约由于合约开发者在编写智能合约transfer函数时产生了有意或者无意的代码漏洞,或者嵌入了恶意的后门代码,使得该合约中的代币买卖存在漏洞,例如只能在交易所买入,无法卖出,或者实际卖出数量与用户所期望数量不相符等.
另一种则是基于智能合约owner权限转移漏洞的攻击事件.几乎所有的智能合约中都有权限控制相关的代码逻辑,如何正确地将权限控制置于自己所开发的代码中,对于开发者来说相当重要,如果设计不当就会产生权限篡改的风险.著名的跨链互操作协议Poly Network[2],就曾因为权限控制代码逻辑漏洞,被攻击者修改了合约属主权限,从而在以太坊、币安智能链以及Polygon这3大区块链平台上分别被盗走2.5亿美元、2.7亿美元、8 500万美元的加密资产,损失总额高达6.1亿美元[3].
目前对于智能合约漏洞的研究往往仅限于一些传统的特定漏洞,例如:整数溢出、重入、delegatecall导致的意外代码执行、合约构造函数与合约名不一致等漏洞[4].这些漏洞有一部分已经被更新版本的智能合约语言编译器消除,例如Solidity在0.8版本将默认内置整数溢出检查[5],开发者不再为因疏忽引起的整数溢出漏洞而担惊受怕,同时在Solidity 0.4.22版本之后,开发者定义的合约构造函数名也不需要与合约名一致,而是统一用constructor代替,消除了合约构造函数与合约名不一致漏洞的产生.另外为避免一些漏洞,例如未检查返回值、重入漏洞等,智能合约Solidity网站也发布了简洁、可靠的安全编程规范[6-7]和框架避免写出有漏洞的合约.
可以看出很传统的智能合约漏洞模式已经逐渐被消除和有效地规避,并且在目前的研究中发现并未产生过大的危害[8].但目前各种新型的智能合约漏洞正在逐渐涌现,例如本文提到的造成欺诈性代币攻击的代币买卖漏洞和造成权限篡改攻击的智能合约owner权限转移漏洞,而目前的研究很少能够总结出一套全面的、有效的自动化检测方案.
鉴于此,本文提出了一种针对智能合约字节码和源码的基于静态语义分析和符号执行的智能合约漏洞检测方法,可以用来检测智能合约的代币买卖漏洞和owner权限转移漏洞.
本文的主要贡献在于:
1) 对于智能合约代币买卖漏洞和权限转移漏洞进行研究,总结了代币买卖漏洞和owner权限转移漏洞模型;
2) 在智能合约字节码和源码层面,提出了针对代币买卖漏洞和owner权限转移漏洞的自动化检测工具;
3) 调研和总结了目前在以太坊和币安智能链2大区块链平台上的代币买卖漏洞和owner权限转移漏洞的常见利用情况以及攻击背景和手法.
1 相关工作
在基于符号执行的智能合约自动化安全审计技术领域,Luu等人[9]提出了基于符号执行的Oyente工具来自动化检测智能合约中存在的整数溢出、时间戳依赖、交易顺序依赖以及重入等漏洞;Muller[10]提出了另外一款基于符号执行技术的工具Mythril,以检测整数溢出、代码重入等常见安全问题;Tsankov等人[11]开发了一种基于符号执行的智能合约自动化检测工具Securify,该工具可以针对给定的性质来验证智能合约的行为是否安全;Nikolic等人[12]实现了一种基于符号分析和程序验证器的工具Maian,该工具通过分析智能合约函数之间调用的字节码序列来检测可能存在的安全漏洞;Zhou等人[13]设计了一种名为SASC的静态分析工具,该工具同样基于符号执行技术,用于智能合约逻辑分析,可以生成函数之间调用流图,以帮助查找智能合约潜在的逻辑漏洞.除此之外,还有Manticore[14],VerX[15]等智能合约自动化检测工具也是采用符号执行技术.
在基于程序静态分析的智能合约自动化安全审计技术方面,Tikhomirov等人[16]提出了一种可扩展的静态分析工具SmartCheck,将solidity源代码转换为基于XML的中间表示形式,然后根据定义的XPath进行漏洞检测;Feist等人[17]提出了一种静态分析框架Slither,它集成了大量漏洞检测模型,通过中间表示(SlithIR)可实现简单、高精度的分析,并提供一个API来轻松编写自定义合约分析;Kalra等人[18]提出了智能合约的静态分析框架ZEUS,它能够自定义用户策略,将附加上用户策略的合约源码转换为LLVM-IR的中间表示,然后结合LLVM-IR的分析工具进行代码分析和漏洞检测.该方案在进行合约源码到中间表示转换时容易失真,LLVM-IR无法完全模拟智能合约的代码和运行环境.
在基于模糊测试的智能合约漏洞检测领域,Trail of Bits安全团队[19]提出了以太坊智能合约模糊测试框架Echidna,通过静态分析和模拟执行智能合约源码来自动化生成调用合约方法的交易数据.而ContractFuzzer[20]将模糊测试和漏洞检测方式结合,通过随机生成交易数据、交易发起者、交易金额和日志监测来检测有无漏洞触发.在模糊测试种子生成策略方面,ILF[21]采用基于神经网络的机器学习算法,对通过符号执行后生成的高覆盖率交易序列进行学习,从而生成更好的模糊测试策略.但是模糊测试方法相较于符号执行,依旧存在着路径覆盖不够全面的问题.
以上大多数工具都是针对传统的以太坊智能合约漏洞,如算术溢出、重入、交易顺序依赖、delegatecall导致意外代码执行[22]等,但对于其他的一些危害较为严重的新型漏洞模式则不能准确识别,比如代币买卖漏洞、owner权限转移漏洞等.
2 漏洞分析及其检测方法
2.1 代币买卖漏洞
2.1.1 漏洞产生的原因及危害
代币买卖漏洞通常发生在遵循ERC-20代币标准的智能合约的transfer函数调用流中.
ERC-20代币标准是以太坊区块链上一种通用的同质化代币标准,通过遵循ERC-20代币标准,可以让不同智能合约中发行的代币都具有相同的类型和接口.
ERC-20代币标准中定义了标准函数和标准事件,如表1所示.其中涉及到代币买卖的函数是transfer和transferFrom函数,这2种函数一般会调用同一种开发者自定义的子函数“_transfer(address sender,address recipient,uint256 amount)”,即执行sender地址向recipient地址转账amount数量的代币操作.
表1 ERC-20代币标准
本文将transfer和transferFrom函数调用中的程序行为抽象为“_transfer(address from,address to,uint256 amount)”函数行为.
图1所示为一个代币买卖漏洞代码片段:
图1 代币买卖漏洞代码片段
代码第4行条件分支语句中当sender地址为owner时,执行正常的第5行和第6行转账操作;如果sender地址不为owner时,执行第8行和第9行操作,即接收方recipient地址余额仅增加amount的一半.
代币买卖漏洞是指类似上述代码的这种情况:实际转账数量与用户所期望数量不相符,收取了高额的转账手续费;只能特定账户进行转出,普通用户只能转入,不能转出等.
在DeFi领域,大多数代币都在去中心化交易所(DEX)中建立了交易对和流动池,在去中心化交易所中用户可以使用其他价值较高的数字货币(例如wBTC,wETH,wBNB,USDT等)来买入这些代币,一旦这些代币合约中出现买卖漏洞,买入后无法卖出,或者卖出数量远低于实际数额,那么用户的wBTC,wETH,wBNB,USDT等数字货币将会锁在合约中,对用户的数字资产造成巨额损失.
2.1.2 漏洞分析与漏洞模型
将合约源代码转换为字节码,第5,6,8,9行关于“_balances”的修改存储操作在字节码层面的模型特征为
SHA(memory)→SLOAD(key)→ADD(value,amount)→SSTORE(key,new_value)
全局变量_balances在Solidity智能合约中是一个address映射到uint256类型的Mapping数据结构,其在以太坊虚拟机(EVM)底层通过key-value方式进行存储,value就是映射中对应的uint256数值,key值是将address与全局变量_balances的槽位拼接后进行哈希(SHA3指令)得到的256位数值.SHA3指令对memory中存储的值进行哈希,SLOAD指令则通过哈希后得到的key值在storage中进行检索相应的value.对检索到的value值进行加或减(SUB指令)操作,然后将新的value值存储(SSTORE指令)到key对应的变量_balance中.
漏洞点在于在SSTORE指令时,任意recipient地址(0地址和sender地址这种特殊地址除外)以及_balance的槽位作为key时,其存储的value数值不等于原value数值与用户调用transfer函数时amount的加和.
2.1.3 漏洞检测方法
本文代币买卖漏洞检测方法步骤如图2所示:
图2 代币买卖漏洞检测方法
图2中:
1) 对智能合约源代码进行编译形成字节码;
2) 对字节码和操作码进行静态分析,构建基本块和控制流图;
3) 通过ERC-20标准函数签名对第2步静态分析处理后的contract对象匹配出符合ERC-20标准的代币合约;
4) 对符合ERC-20标准的代币合约的balanceOf(address_owner)进行静态语义分析,确定在执行“_balance[_owner]”时的_balance变量的槽位;
5) 基于符号执行技术,符号化transfer以及transferFrom函数参数,符号化msg.sender地址;
6) 建立约束条件,求解当满足
balance[_to]==balance[_to]+amount
的msg.sender的值集合情况;
7) 当满足模型的msg.sender值为有限个,则上报代币买卖漏洞.
2.2 owner权限转移漏洞
2.2.1 漏洞产生的原因及危害
在编写智能合约的过程中,合约的开发者一般会设置一个“owner”值,该值所代表的地址拥有一些特权,如转账、函数调用等.如果对“owner”值的修改没有施加限制条件,那么攻击者能够修改该值为自己的地址,从而攻击者会利用这些特权来攻击合约并获取利益.
图3所示为owner权限转移漏洞代码片段:
图3 owner权限转移漏洞代码片段
图3中第5行表明在构建函数时可以将owner权限设置给合约创建者,同时第12行mint函数附加了修饰符onlyOwner,第9行判断只有当调用者等于owner时才可以进行第13,14行的mint函数操作.mint函数用于属主向某个地址铸造更多的代币.在第16行setOwner函数可以修改owner变量,但是未附加修饰符onlyOwner,使得任意地址可以调用setOwner函数对owner变量进行修改,从而让属主转变为攻击者地址,再调用mint函数就可以铸造大量代币分发给攻击者地址.
2.2.2 漏洞分析与漏洞模型
经过调研发现,owner所在的storage槽位一般与onlyOwner修饰符以及构造函数中msg.sender相关.在构造函数中,owner一般由msg.sender或者普通地址类型变量进行初始化.在onlyOwner修饰符中,owner一般被用于与msg.sender值比较,其在字节码层面的模型特征为
EQ(owner,msg.sender)→ISZERO→JUMPI.
同时从字节码层面分析,owner权限转移漏洞模型可以总结为:合约执行过程中存在SSTORE指令,并且SSTORE指令的key值与owner所在的storage槽位相等.
2.2.3 漏洞检测方法
本文owner权限转移漏洞检测方法的步骤如图4所示:
图4 owner权限转移漏洞检测方法
图4中:
1) 对智能合约源代码进行编译形成字节码.
2) 对字节码和操作码进行静态分析,构建基本块和控制流图.
3) 确定“owner”的存储位置.详细过程为:在合约内,一般会对“owner”变量赋值为msg.sender,即合约的创建者,msg.sender由CALLER操作码压入EVM栈中,赋值操作由SSTORE操作码完成.在操作码序列中寻找CALLER操作码,CALLER会将msg.sender的值放入栈中,对栈中该数据进行跟踪.在操作码序列中寻找SSTORE操作码,SSTORE操作会从栈中获取key和value.如果value为msg.sender或者经过AND运算后的20 B变量(一般为address类型变量),那么key就是“owner”在storage中的存储位置.同时通过第2步静态语义分析中寻找的onlyOwner修饰符,对与msg.sender变量比较的全局变量槽位进行捕获.将上述这2种方式确定的存储位置记录下来,取其交集作为owner变量的存储位置供后续步骤使用.
4) 符号执行,根据步骤3)确定的存储位置,判断写操作的存储位置是否是该位置.遍历寻找SSTORE操作码,SSTORE操作码会从EVM栈中取出key,判断key是否与步骤1)中找到的“owner”的存储位置一致.如果是,则记录该路径.
5) 对满足上述条件的路径进行约束求解,有解则报告该漏洞.根据当前的约束条件进行求解,如果有解则表明存在任意调用者可以对“owner”的值进行修改,即存在漏洞.
3 漏洞检测工具的设计与实现
3.1 总体架构
该漏洞检测模型总体架构如图5所示,主要包括合约收集、预处理、静态分析、符号执行和漏洞模型分析5个模块:
图5 漏洞检测模型总体架构
3.2 模块设计与实现
3.2.1 合约收集模块
合约收集模块集成了以太坊infra节点和币安智能链节点,可以通过各种方式对合约源代码和字节码进行收集,收集方式包括本地读取、通过etherscan收集源代码、通过合约地址收集链上字节码和etherscan源码及字节码、通过块高度区间收集链上合约源码和字节码等.
同时集成了Ethereum Signature Database[23]的API,可以通过字典查询的方式对字节码中的函数签名进行猜解.
3.2.2 预处理模块
预处理模块的主要功能是对输入的数据进行预处理,包括输入校验和数据处理.对于输入的智能合约solidity源码数据和EVM字节码数据,首先校验其合法性.如果输入的数据为solidity源码,则需要使用solc编译器编译为EVM字节码.
预处理模块执行流程如图6所示:
图6 预处理模块
3.2.3 静态分析模块
静态分析模块构建基本块和控制流图,基本块只包含顺序执行的指令,只有1个入口和1个出口,入口处于基本块的第1条指令,出口位于基本块的最后1条指令,中间不出现任何分叉.如果遇到跳转指令(JUMP或JUMPI),那么结束当前基本块,将该指令作为当前基本块的最后1条指令,并分叉出1个新的基本块,将JUMPDEST指令作为新基本块的第1条指令.基本块B和基本块C之间存在1条边则构建形成基本块控制流.
3.2.4 符号执行模块
经过预处理模块得到字节码数据以及静态分析处理后的基本块和控制流图,模拟EVM执行字节码,并创建当前的执行状态,包括Stack,Memory和Storage.每个操作码指令都对应1个状态,通过符号执行,可以获取每个状态下Stack,Memory和Storage所存储的内容.然后利用该状态空间的基本代码块和路径约束条件集,添加约束和路径集合形成新的符号执行控制流图.如图7所示:
图7 符号执行模块
3.2.5 漏洞分析模块
漏洞分析模块包括代币买卖漏洞检测模块和owner权限转移漏洞模块.
漏洞分析模块通过符号执行以及静态分析暴露的接口,对EVM的stack,memory,storage,calldata,sender等进行符号化,同时由于SMT对哈希指令SHA3的支持较弱,添加SHA3的符号化组件,在符号化表达式语义层面对涉及SHA3的约束进行求解.最终利用Z3求解器对约束进行求解并上报漏洞,如图8所示:
图8 漏洞分析模块
4 工具测试与实验分析
4.1 测试环境及样本类型
为验证工具的有效性以及性能等指标,本文使用以下硬件和软件环境进行测试,如表2和表3所示:
表2 硬件环境
表3 软件环境
本文在以太坊主网的去中心化交易所Uniswap以及币安智能链主网的去中心化交易所PancakeSwap各抽取50个代币合约作为代币买卖漏洞的测试样本,一共100个代币合约.
本文爬取了以太坊主网区块高度13 000 000~13 001 000中新部署的合约,共计172个,同时在此样本基础上,增加12个与owner权限转移漏洞相关的智能合约CVE,作为owner权限转移漏洞的检测样本.
代币买卖漏洞在CVE库中没有相应案例,故只使用链上交易所合约样本.
4.2 测试结果与分析
4.2.1 代币买卖漏洞
对代币买卖漏洞检测工具进行测试,结果如表4所示:
表4 代币买卖漏洞测试结果
由表4可发现,代币买卖漏洞检测工具运行用时较长,误报率较高.
经过分析,运行用时长主要是符号执行耗时过长.
误报率过高主要原因有2个:一是由于大量代币合约在转账时设置手续费,使得实际转账金额与原数额有差别.经粗略统计,对于正常合约手续费设置基本在25%以内.二是因为一些合约设置了白名单和黑名单,限制了一些地址的转账权.
以代币合约DogeKing(币安智能链地址0x641 EC142E67ab213539815f67e4276975c2f8D50)为例,其代码片段如图9所示:
图9 代币合约DogeKing代码片段
该合约就是收取手续费类型的代币合约.第7~18行,合约对不包含在_isExcludedFromFees映射中的地址收取转账手续费.由于该合约转账手续费不高,其用户也普遍知晓并遵从其手续费的机制,因此该合约不属于代币买卖漏洞.
以检出的漏洞合约(币安智能链地址0xe9E 3666f64c699529c9d3f9e2c506FF13fDe0E61)为例,对漏洞样本进行分析,由于该合约未开源,其字节码反编译后的代码片段如图10所示:
图10 0xe9漏洞合约反编译后的代码片段
在第2~6行、第7~11行、第12~21行,均对用户转账行为进行了限制.第2行的变量unknown1e445a90是一个全局开关,值由属主控制,当为false时,所有普通用户才可以转账.第7行是一个特权数组,在数组中的地址才可以进行转账.第12~21行,全局变量unknown1b355427Address存储属主地址,如果转账用户为属主才允许转账.在币安智能链上观测该合约的交易可以发现,该合约代币approve函数同样存在上述类似的恶意限制,balanceOf函数进行了恶意改写,balanceOf函数代码片段如图11所示.
图11 balanceOf函数代码片段
在满足全局开关变量unknown1e445a90为false(第2行所示)、查询余额的地址为属主(第3行所示)、查询余额的地址属于特权数组(第4行所示)3个条件之一的情况下,才会返回真实余额(第6行所示),否则所有用户将返回1个固定值的全局变量(第5行所示).恶意balanceOf函数让所有普通用户观察到自己的地址内有大量代币,诱使他们通过去中心化交易所进行投资买入,却无法卖出.该合约属于有后门的恶意合约.
4.2.2 owner权限转移漏洞
对owner权限转移漏洞检测工具进行测试,结果如表5所示:
表5 owner权限转移漏洞测试结果
由表5可知,本文工具运行用时较长,误报率较低,检出率低.
经过分析,运行用时长主要是符号执行耗时过长,并且由于符号执行中对所有的SSTORE都尝试进行约束求解,使得时间消耗相较于代币买卖漏洞的检测用时高.检出率低和误报率低原因一是因为模型针对性强,准确度高,其次是因为OpenZepplin区块链应用标准中规范了访问控制权限的开发模式,帮助开发者避免一些容易疏忽的权限漏洞.
以检出的CVE-2021-34273漏洞合约为例,对漏洞样本进行分析,漏洞合约代码片段如图12所示.
图12 检出的CVE-2021-34273漏洞合约代码片段
该合约为ERC-20代币合约BTC2X(B2X)的一部分,用来初始化合约中的owner值,以及定义限定修饰符onlyOwner,并且可以将ownership转移给其他地址.
在漏洞合约中使用owner变量来存储合约拥有者地址,但由于构造函数名的错误,合约允许任何人调用owned()函数来修改合约的所有者,存在owner权限漏洞.第4行代码中,由于owned()函数访问修饰符为public,从而任何人可以调用该函数修改owner值为自己账户的地址.当恶意攻击者调用该函数修改owner值为自己账户的地址后,便可以调用代币合约中的其他函数来获利.
5 结 论
针对代币买卖的后门漏洞以及owner权限转移漏洞的检测问题,本文提出了一种源代码和字节码层面的自动化检测方法.通过静态语义分析与符号执行相结合的技术,对漏洞点进行检测,并且通过符号执行自动化形成利用路径.通过在以太坊和币安智能链上主网合约进行测试,发现了新的代币买卖漏洞合约,通过实验,与owner权限转移漏洞相关的CVE也得到自动化的检测和复现.