博客转移新地址
渣渣学习以太坊,参考大佬文章并综述,综述2333
参考列表:
区块链安全 - DAO攻击事件解析(模拟过程)
智能合约初体验(基础概念)
从技术角度剖析针对THE DAO的攻击手法(实际攻击分析)
基础概念
1.以太坊两种不同的账户
外部账户,由人类控制(通过拥有私钥)
合约账户,由代码控制(代码即法律)只有合约账户才有fallback功能
2.合约编程语言
Solidity: 类JavaScript,这是以太坊推荐的旗舰语言,也是最流行的智能合约语言。具体用法参加Solidity文档。
3.以太币和Gas
以太币: ETH,以太坊中的虚拟货币,可以购买和使用,也可以与真实货币交易。以太币的走势图
Gas:相当于手续费。在以太坊执行程序以保存数据都要消耗一定量的以太币。这个机制可以控制区块链中计算的数量,保证效率。
4.智能合约与DApp
DApp——Dimensional Assessment Of Personality Pathology
DApp流程:
用Solidity(或其他语言)编写智能合约(后缀为.sol)
用solc编译器将.sol合约编译成EVM字节码
编译好的字节码回送给dapp前端
前端将编译好的智能合约部署到区块链中
区块链返回智能合约地址+ABI(合约接口的二进制表示。合约接口用JSON表示,包括变量,事件和可以调用的方法)
前端通过Address+ABI+nonce,调用智能合约。智能合约开始处理。
基础知识说明
1.跨合约调用问题
智能合约之间的调用本质上是外部调用,可以使用message call或者创建智能合约对象的形式进行调用。
使用message call
比如合约1调用合约2的某个方法:
bytes4 methodId = bytes4(keccak256("increaseAge(string,uint256)"));
return addr.call(methodId,"jack", 1);
还原智能合约对象 如果已知合约的地址,可以通过如下方式获取到合约对象:
Contract1 c = Contract1(AddressOfContract1) ; c.foo() ; //跨合约调用
2.智能合约发送ETH
我们可以在智能合约中用代码向某个地址(这个地址可以是人,也可以是智能合约)发送以太币,比较常见的两个方式是:
调用send函数
比如:msg.sender.send(100)使用message call
msg.sender.call.value(100)()
这两个方式不同的是发送的gas数量,gas就是执行opcode需要花费的一种币,称作为gas也特别形象。当调用send方法时,只会发送一部分gas,准确地来讲,是2300gas,一旦gas耗尽就可能抛出异常。
而使用message call的时候,则是发送全部的gas,执行完之后剩余的gas会退还给发起调用的合约。
我理解的是,发多少用多少和全部发返剩余的关系。
3.fallback函数
智能合约中可以有唯一的一个未命名函数,称为fallback函数。该函数不能有实参,不能返回任何值。如果其他函数都不能匹配给定的函数标识符,则执行fallback函数。
当合约接收到以太币但是不调用任何函数的时候,就会执行fallback函数。如果一个合约接收了以太币但是内部没有fallback函数,那么就会抛出异常,然后将以太币退还给发送方。
下面就是一个fallback函数的代码示例:
contract Sample{ function () payable{ // your code here } }
一般单纯使用message call或者send函数发送以太币给合约的时候,没有指明调用合约的某个方法,这种情况下就会调用合约的fallback函数。
攻击模拟
首先是存在漏洞的智能合约代码,Bank:
说明:用户可以通过addToBalance方法存入一定量的以太币到这个智能合约,通过withdrawBalance方法可以提现以太坊,通过getUserBalance可以获取到账户余额。
感受一下gas的代价:
出问题的是withdrawBalance方法,特别是在修改保存在区块链的balances的代码是放在了发送以太币之后。 攻击代码如下:
这里的deposit函数是往Bank合约中发送10wei。withdraw是通过调用Bank合约的withdrawBalance函数把以太币提取出来。注意看这里的fallback函数,这里循环调用了两次Bank合约的withdrawBalance方法。
攻击模拟过程
(1)假设Bank合约中有100wei,攻击者Attack合约中有10wei
(2)Attack合约先调用deposit方法向Bank合约发送10wei
(3)之后Attack合约调用withdraw方法,从而调用了Bank的withdrawBalance方法。
(4)Bank的withdrawBalance方法发送给了Attack合约10wei
(5)Attack收到10wei之后,又会触发调用fallback函数
(6)这时,fallback函数又调用了两次Bank合约的withdrawBalance,从而转走了20wei
(7)之后Bank合约才修改Attack合约的balance,将其置为0
通过上面的步骤,攻击者实际上从Bank合约转走了30wei,Bank则损失了20wei,如果攻击者多嵌套调用几次withdrawBalance,完全可以将Bank合约中的以太币全部转走。
攻击手法还原
节选自从技术角度剖析针对THE DAO的攻击手法
实际DAO攻击的产生是由于攻击者创建了childDAO并将Ether持续转入其中,这是目前唯一可行的提取Ether的机制,所以关注点从splitDAO函数开始。
splitDAO会创建childDAO(如果不存在的话),将分裂者拥有的Ether转入childDAO中,根据白皮书的设计,splitDAO的本意是要保护投票中处于弱势地位的少数派防止他们被多数派通过投票的方式合法剥削。通过分裂出一个小规模的DAO,给予他们一个有效投票的机制,同时仍然确保他们可以获取分裂前进行的对外资助产生的可能收益。但通往地狱的道路就这样用鲜花铺就了。根据BLOG 2,在DAO.sol中,function splitDAO函数有这样一行:
先回退,后归零,和模拟案例的情况就是一样的。来看看withdrawRewardFor函数:
paidOut[_account] += reward 在原来代码里面放在payOut函数调用之后,最新github代码中被移到了payOut之前。
再看payOut函数调用。rewardAccount的类型是ManagedAccount,在ManagedAccount.sol中可以看到:
对_recipient发出call调用,转账_amount个Wei,call调用默认会使用当前剩余的所有gas,此时call调用所执行的代码步骤数可能很多,基本只受攻击者所发消息的可用的gas限制。
攻击就很简单了,黑客创建自己的黑客合约HC,该合约带有一个匿名的fallback函数。根据solidity的规范,fallback函数将在HC收到Ether(不带data)时自动执行。此fallback函数将通过递归触发对THE DAO的splitDAO函数的多次调用(但不会次数太多以避免gas不够),过程中还应该需要记录当前调用深度以控制堆栈使用情况。
提交split proposal:
—> splitDAO函数(No. 1)
—> withdrawRewardFor函数 (No. 1,黑客的dao余额和dao总量此时没变!)
—> payOut 函数(No. 1,向HC发送以太第一次)
—> HC的fallback函数 (No. 1)
—->如果递归未达预设深度:调用splitDAO函数(No. 2)
—> withdrawRewardFor函数(No. 2, 黑客dao余额等仍然没变!)
—> payOut函数(No. 2, 向HC发送以太第二次)
—> HC的fallback函数 (No. 2)
—> (继续递归)
转入childDAO的钱在一定时间后根据原合约可以提取,黑客收割韭菜的时候到了。
防范和思考
攻击得逞的因素有二:一是dao余额扣减和Ether转账这两步操作的顺序有误,二是不受限制地执行未知代码。
应用代码顺序方面,应先扣减dao的余额再转账Ether,因为dao的余额检查作为转账Ether的先决条件,要求dao的余额状况必须能够及时反映最新状况。在问题代码实现中,尽管最深的递归返回并成功扣减黑客的dao余额,但此时对黑客dao余额的扣减已经无济于事,因为其上各层递归调用中余额检查都已成功告终,已经不会再有机会判断最新余额了。
不受限制地执行未知代码方面,虽然黑客当前是利用了solidity提供的匿名fallback函数,但这种对未知代码的执行原则上可以发生在更多场景下,因为合约之间的消息传递完全类似于面向对象程序开发中的方法调用,而提供接口等待回调是设计模式中常见的手法,所以完全有可能执行一个未知的普通函数。