从solidity说话个性深度解读以太坊智能合约缝隙道理和攻击利用
颁布功夫 2018-08-02随着区块链、以太坊技术的鼓起和不休成熟,,,,,,,安全问题也随之而来,,,,,,,今年智能合约缝隙已经让多个区块链项目价值瞬间归零。。。。。。智能合约的开发说话、设计模式、运行机造都与传统利用有较大差距,,,,,,,它寂仔传统的安全风险(如整数溢出等),,,,,,,又有怪异的新型风险(如私有变量不“私佑妆和特殊类型变量覆盖等)。。。。。。研发人员若是不能深刻理解这些主题道理,,,,,,,则很容易编写出存在缝隙的智能合约;;;;;;;恶意合约也能够通过这种步骤留下荫蔽缝隙,,,,,,,糊弄合约投资人并暗地里收割。。。。。。本文以WCTF2018的一路智能合约缝隙赛题[1]为例,,,,,,,从solidity说话个性启程,,,,,,,深度解读以太坊智能合约缝隙道理和攻击利用。。。。。。
2 缝隙合约分析
该合约是一个银行类合约,,,,,,,用户能够存入eth到该合约,,,,,,,并在存入到期之后取出。。。。。。原题对该合约描述如下:
该合约中存在缝隙,,,,,,,攻击者利用缝隙能够盗取合约中的所有余额。。。。。。缝隙涉及到整数溢出、变量覆盖以及由变量覆盖导致的变量相互影响。。。。。。
合约源码如下:
要提取合约的全数合约余额,,,,,,,confiscate 函数是关键,,,,,,,但该函数挪用成功必须满足:
? msg.sender == owner
? secret == _secret
? now >= balances[account].deposit_term + 1 years
攻击者能够通过合约存储接见、整数溢出和变量覆盖来顺次机关上述前提。。。。。。
2.1 solidity全局变量存储
在BelluminarBank合约中,,,,,,,一共有4个全局变量,,,,,,,别离是balances、head、owner、secrete。。。。。。它们的默认接见属性是private,,,,,,,看上去只有合约自己可能接见这些变量。。。。。。事实上,,,,,,,合约的所有变量数据都是公开存储在链上的区块中,,,,,,,任何人都能够通过接见存储数据来获得这些变量的值[2]。。。。。。在solidity说话中,,,,,,,全局变量都存储在storage中,,,,,,,凭据solidity的变量存储规定,,,,,,,定长的变量在storage中是挨次存储的,,,,,,,数组变量在storage中其索引地位存放的是其数组长度(拜见[3])。。。。。。该合约storage中的变量存储布局如下:
因而,,,,,,,secrete并不是一个不成获取的私罕见据,,,,,,,攻击者只必要接见该合约storage中的数据就能够机关confiscate 函数的secret == _secret前提。。。。。。
2.2 solidity全局变量覆盖
BelluminarBank合约中的confiscate函数要求挪用者必须是合约占有者能力够进行余额提取操作,,,,,,,看上去攻击者是无法提取的。。。。。。然而,,,,,,,由于solidity说话的部门变量存储个性,,,,,,,导致本合约的owner变量能够被批改,,,,,,,覆盖问题呈此刻 invest 函数中。。。。。。
首先来看solidity部门变量覆盖全局storage的问题。。。。。。solidity说话的变量存储有一个个性,,,,,,,即数组、映射、结构体类型的部门变量默认是引用合约的storage [4],,,,,,,而全局变量默认存储在storage中。。。。。。因而,,,,,,,若是这些部门变量未被初始化,,,,,,,则它们将直接指向storage,,,,,,,批改这些变量就是在批改全局变量。。。。。。
以如下的单一合约test为例,,,,,,,函数test1中界说了一个部门结构体变量x,,,,,,,但是没有对其进行初始化。。。。。。凭据solidity的变量存储规定,,,,,,,这时辰x是存储在storage中的,,,,,,,并且是从索引0起头,,,,,,,那么对其成员变量x,y赋值之后,,,,,,,刚好覆盖了全局变量a和b。。。。。。有兴致能够在 remix 中在线对本合约进行调试。。。。。。
pragma solidity 0.4.24;
contract test {
struct aa{
uint x;
uint y;
}
uint public a = 4;
uint public b = 6;
function test1() returns (uint){
aa x;
x.x = 9;
x.y = 7;
}
}
在invest函数的else分支中,,,,,,,使用了一个部门结构变量investment。。。。。。该部门变量在当前执行分支中并没有被初始化,,,,,,,默认指向合约的storage。。。。。。执行中对该变量的成员赋值就会直接覆盖全局变量,,,,,,,覆盖关系为:
同时,,,,,,,在变量覆盖之前必须满足如下前提,,,,,,,即存款期限是最末一个存款纪录的期限后一年:deposit_term >= balances[balances.length - 1].deposit_term + 1 years。。。。。。由于deposit_term是用户提供的,,,,,,,轻松就能够满足。。。。。。
所以,,,,,,,通过精心机关invest函数的参数就能够覆盖stroage中的sender,,,,,,,从而扭转该合约的占有者为攻击者,,,,,,,突破confiscate 函数的msg.sender == owner限度。。。。。。
2.3 整数溢出
在BelluminarBank合约源码的confiscate函数还有另表一个如下的功夫限度,,,,,,,即必须在存款满一年后能力提�。。。。。。�,,,,,now >= balances[account].deposit_term + 1 years。。。。。。
上一节用于全局变量覆盖的存款操作使得balances中最末一个存储纪录的期限已经是1年后,,,,,,,即攻击者至少在2年后能力挪用confiscate函数进行提款。。。。。。与此同时,,,,,,,deposit_term在赋值给部门变量的时辰会把全局变量head覆盖为超大的数,,,,,,,这也使得后续的for (uint256 i = head; i <= account; i++)循环处置无法提取全数的存款,,,,,,,由于head不为0。。。。。。
显然,,,,,,,必须把head覆盖为0能力提取全数的存款,,,,,,,即invest函数的deposit_term参数必须为0。。。。。。但若是该参数为0,,,,,,,又无法满足invest函数的全局变量覆盖执行的前提deposit_term >= balances[balances.length - 1].deposit_term + 1 years。。。。。。
仔细分析可发现,,,,,,,若是balances[balances.length - 1].deposit_term+ 1 years刚好蹬宗0,,,,,,,则上述的前提恒为真。。。。。。显然,,,,,,,balances[balances.length - 1].deposit_term只有取值为(uint256_max – 1 years + 1),,,,,,,就会导致相加后的值为uint256_max+1。。。。。。这个了局会超过uint256的表白空间,,,,,,,产生溢出导致最后的值为0。。。。。。
因而,,,,,,,攻击者先做第一次存款,,,,,,,把balances最后一项的deposit_term设置为特殊值;;;;;;;而后做第二次存款,,,,,,,deposit_term传入0值,,,,,,,就能触发整数溢出,,,,,,,绕过变量覆盖前提限度并批改head为0值。。。。。。
2.4 “变量纠缠”的副作用
在全局变量覆盖中,,,,,,,很容易产生“变量纠缠”景象,,,,,,,从而触发一些容易被忽视的副作用。。。。。。这里以一个单一合约test为例,,,,,,,函数testArray中依然存在结构体部门变量a覆盖全局变量x的情况。。。。。。但由于x是数组变量,,,,,,,其直接索引的storage存储地位仅存储其数组长度,,,,,,,也就是a.x只会覆盖x的数据长度,,,,,,,而a.y将覆盖变量num。。。。。。
在testArray函数中,,,,,,,赋值操作a.x = 5时,,,,,,,由于x.length与变量a.x处于统一存储地位,,,,,,,赋值后数组x的长度造成了5。。。。。。接下来,,,,,,,赋值a.y,,,,,,,并将变量a参与到数组x。。。。。。所以变量a现实上参与到了数组x索引为5的地位。。。。。。若是调试testArray函数执行,,,,,,,会发此刻函数执行结束之后,,,,,,,x[5].x = 6, x[5].y = 7。。。。。。
这是为什么呢????????明明代码中赋值写的是 a.x = 5,,,,,,,a.y = 7。。。。。。这就是全局变量x和部门变量a形成了“纠缠”,,,,,,,首先是部门变量a批改导致全局变量x扭转,,,,,,,而后是全局变量x批改导致终部门变量批改,,,,,,,最后把批改后的部门变量又存储到批改后的全局变量。。。。。。这里即是,,,,,,,赋值操作a.x = 5时,,,,,,,把数组x的长度造成了5; 接下来x.push操作,,,,,,,现实上是先将该数组x的长度加1,,,,,,,此时a.x = 6; 最后再把a.x = 6, a.y=7参与到x[5]。。。。。。所以,,,,,,,存入数据的x就是新数组的长度6。。。。。。
pragma solidity 0.4.24;
contract test {
struct aa{
uint x;
uint y;
}
aa [] x;
uint public num = 4;
function testArray() returns (uint){
aa a;
a.x = 5;
a.y = 7;
x.push(a);
}
}
3 缝隙利用方式
在第2节中对合约 BelluminarBank存在的几个缝隙进行了分析,,,,,,,下面将注明若何利用这个缝隙提取合约的全数余额,,,,,,,这里在Remix在线编译环境中部署该合约,,,,,,,并演示其利用方式。。。。。。
首先部署合约,,,,,,,在部署参数中设置secrete 为“0x01”,,,,,,,deposit_term为1000,,,,,,,msg.value为 31337 wei。。。。。。
部署合约后,,,,,,,合约的全局变量如下图所示:
这样,,,,,,,合约目前的余额是 31337 wei,,,,,,,合约占有者的地址为:0xca35b7d915458ef54 0ade6068dfe2f44e8fa733c。。。。。。
下面起头必要机关前提使得攻击者能够成功挪用confiscate函数。。。。。。
步骤1: 覆盖owner并机关整数溢出前提
要想转走合约余额,,,,,,,首先必须批改合约的owner。。。。。。利用部门结构体 investment 批改合约owner,,,,,,,需满足前提:
(1)account < head or account >= balances.length
(2)deposit_term >= balances[balances.length – 1].deposit_term + 1 years
设置攻击者(0x1472…160C)的invest挪用参数如下:
? msg.value = 1 wei (由于在合约初始化时owner已经存入一笔金额,,,,,,,所以此时balances数组长度为1,,,,,,,为了不扭转balances数组长度,,,,,,,这里依然将其设置为1 we i
? depositsit_term = 2^256 - 1 years = 115792089237316195423570985008687907853269984665640564039457584007913098103936 (在步骤2中必要利用这个数值机关溢出,,,,,,,同使剽个值能够使源码中 require 前提得到满足)
? account = 1 (满足前提 account >= balances.length)
挪用之后,,,,,,,新的存款纪录数据将存放在balances数组索引为1的地位。。。。。。此时的balances数组情况和全局storage变量情况如下图所示。。。。。。
能够发现,,,,,,,owner已经批改为攻击者地址,,,,,,,同时head被传入的deposit_term覆盖为一个超大值。。。。。。
而提取余额是从balances数组中head索引起头的存款纪录起头推算数额的。。。。。。显然,,,,,,,为了提取到合约owner的余额,,,,,,,即balances[0]账户的余额,,,,,,,head必须被覆盖为0。。。。。。因而,,,,,,,必要进行第二次storage变量覆盖,,,,,,,批改head。。。。。。
步骤2: 复原head并绕过deposit_term限度
持续设置攻击者挪用invest的参数:
? msg.value = 2wei (同样保障balances的长度覆盖后不出现谬误)
? deposit_term = 0: 复原head
? account = 2 (满足前提 account >= balances.length 即可)
由于在步骤 1 中,,,,,,,已经将balances[1].deposit_term 设置为 2^256 -1 years,,,,,,,因而在第二次挪用 invest 函数时,,,,,,,由于balances[balances.length - 1].deposit_term + 1 years”溢出为0满足了require前提,,,,,,,所以能够成功进行第二次覆盖。。。。。。
这样即满足了挪用confiscate函数的前提msg.sender == owner,,,,,,,通过读取storage很容易获得secrete,,,,,,,前提secret == _secret 也能够满足,,,,,,,同时还沉新覆盖了head使之变为0 。。。。。。
覆盖之后全局storage变量和balances数组如下图所示:
能够发现head已经批改为0了。。。。。。
此刻来看看第三个前提:
now >= balances[account].deposit_term + 1 years
account是传入的数据,,,,,,,目前合约中account数量为3。。。。。。在前面的invest挪用后,,,,,,, balances[2].deposit_term = 0。。。。。。 显然前提 now >= balances[2].deposit_term + 1 years 成立,,,,,,,所以在复原head数据的同时,,,,,,,也绕过了confiscate函数中对于存款期限的判定。。。。。。接下来只有挪用函数confiscate时,,,,,,,设置account 为 2,,,,,,,便可使功夫判断前提满足,,,,,,,同时也能提取所有账户的余额。。。。。。
步骤3: 增长合约余额
经过步骤1和步骤2,,,,,,,似乎攻击者已经能够挪用confiscate函数提取所有余额了,,,,,,,然而现实上是不能的。。。。。。买卖会产生回滚,,,,,,,这是为什么呢????????
仔细分析前面的数据就会发现,,,,,,,步骤1中msg.value为 1 wei,,,,,,,但是最后balances数组中的balances[1].amount 却造成了 2 wei。。。。。。这是由于变量覆盖过程中产生了“纠缠”副作用,,,,,,,由于msg.value覆盖balances数组的长度,,,,,,,balances更新前增长了数组长度,,,,,,,数组长杜字扭转了msg.value,,,,,,,最后导致存入的amount造成了新的数组长度,,,,,,,即2。。。。。。
所以,,,,,,,每次挪用invest函数进行变量覆盖,,,,,,,存款纪录的账目金额都比挪用者现实支付的msg.value大。。。。。。下图是两次挪用invest之后的balances数组情况。。。。。。
从图中能够看出,,,,,,,存款纪录中的账面值会比现实买卖的msg.value多 1 wei。。。。。。通过confiscate函数推算得到的所有账户总额为31342 wei,,,,,,,而现实的合约账户总余额为 31340 wei。。。。。。
为了可能将合约中所有余额提取出来,,,,,,,必要增长合约的真实余额,,,,,,,使其同存款纪录中的余额相称。。。。。。然而,,,,,,,通过invest方式增长的余额城市被计入账面余额,,,,,,,那么怎么在不通过invest函数的情况下增长合约的真实余额呢????????
答案是selfdestruct函数。。。。。。
selfdestruct函数会将该合约的余额转到指定账户,,,,,,,而后从区块链中销毁该合约的代码和storage。。。。。。该函数的官方文档注明[5]如下:
因而,,,,,,,能够机关一个合约,,,,,,,而后在合约中挪用selfdestruct函数将合约的余额转给BelluminarBank合约。。。。。。为此,,,,,,,机关如下合约:
contract donar{
function donar() public payable{
selfdestruct(contractAddr);
}
}
该合约创建后顿时销毁,,,,,,,同时将自己的余额转给银行合约。。。。。。
在 remix 中 编译该合约,,,,,,,同时将 contractAddr代替为银行合约地址。。。。。。而后 在deploy该合约时,,,,,,,设置 msg.value 为2 wei。。。。。。当合约创建又销毁之后,,,,,,,其余额(2wei)将转给银行账户,,,,,,,使银行合约的账面余额和现实余额一致,,,,,,,这样confiscate函数挪用就可能正确执行。。。。。。
Donar合约部署设置如下:
合约部署完之后,,,,,,,BelluminarBank 合约余额如下图:
步骤4:挪用confiscate提取合约余额
经过上面的操作之后,,,,,,,设置confiscate函数的参数为[2,,,,,,,“0x01”]即可将合约的全数余额转走。。。。。。
参考链接:
【1】https://github.com/beched/ctf/tree/master/2018/wctf-belluminar
【2】https://solidity.readthedocs.io/en/v0.4.24/security-considerations.html#private-information-and-randomness
【3】https://medium.com/aigang-network/how-to-read-ethereum-contract-storage-44252c8af925
【4】 http://solidity.readthedocs.io/en/v0.4.24/frequently-asked-questions.html
【5】https://solidity.readthedocs.io/en/v0.4.24/introduction-to-smart-contracts.html?highlight=selfdestruct


京公网安备11010802024551号