前言近年来,区块链攻防出现在各种大型CTF(Capture The Flag,中文一般翻译为Capture,指网络安全技术人员在网络安全领域进行技术比拼的一种竞赛形式)比赛中,出现问题的多为区块链智能合约攻防。本系列文章主要围绕智能合约的攻防来分析智能合约的攻防要点。在前两篇文章中,我们分享了契约反编译和反汇编的基本内容。后续文章中,我们将继续分享智能合约在CTF比赛中的常见问题(再入、整数溢出、空投、可控随机数等。)以及解决方案,相信会给读者带来不一样的收获。
在上一篇文章中,我们分享了CTF竞赛中经常测试的再入大气层漏洞问题。本文继续分享CTF竞赛中的整数溢出题,这也是一种常见的题型。当然,大多数CTF智能合约问题并不仅仅考察单个漏洞的攻击和防御,而是可能涉及多个漏洞的组合。
本文以2018年WCTF BelluminarBank话题为例,与大家分享智能合约整数溢出的问题。解决这个问题不仅需要整数溢出攻击,还需要变量覆盖、权限设置等攻击技术。
标题地址:
由于WCTF智能合约大赛不在ropsten上举行,也没有线上攻防场景,所以在GitHub:https://GitHub . com/beched/CTF/tree/master/2018/wctf-belluminar中已经给出了合约的具体标题介绍和合约源代码。
分析主题建议团队需要对字节码进行逆向工程,并使用以下攻击:
整数溢出绕过存款期限限制;溢出来盖银行老板;存储访问权限以揭示私有属性;部署自杀契约以强制将eth发送到目标契约(以解决余额差异)并不一定需要意外的以太网攻击。如果使用了withdraw()和invest()调用,它们可以得到适当的平衡。这可能是由于一个巨大的错误导致了错误的解决方案:取款()函数没有改变余额数组。但还是要提前利用整数溢出。
合同显示Belluminar银行很小,很特别。它的工作原理如下:
任何人都可以投资任何金额,并应指定存款期限(在此期限之前存款将被锁定);存款期限必须比以前的客户至少长1年;每笔存款都有一个账号;0账户包含31337魏,已被银行所有人(合同创建人)锁定多年;如果存款期限是一年(如果你不取钱),银行老板可以没收你的存款。目标是破解这家银行,清空它的余额。如果成功,机器人会把交易数据里的logo发给你。
代码实用程序可靠性^0.4.23;合同BelluminarBank {结构投资{ uint256金额;uint256存款_期限;地址所有者;}//全局可变投资余额;uint256头;私人所有者地址;bytes16私人秘密;//secret可以读取函数belluminarbank (bytes16 _ secret,uint 256 deposit _ term)public { secret=_ secret;owner=msg.senderif(msg . value 0){ balances . push(Investment(msg . value,deposit_term,msg . sender));} }函数bankBalance()公共视图returns (uint256) {返回地址(this)。平衡;}//局部变量覆盖全局变量函数Invest (uint256account,uint 256 deposit _ term)public payable { if(account=head account balances . length){ investment storage investment=balances;投资.金额=消息.价值;} else {if(balances.length 0) {//存在整数溢出require(deposit _ term=balances . deposit _ term 1 years);}//局部变量投资
ment.amount = msg.value; investment.deposit_term = deposit_term; investment.owner = msg.sender; balances.push(investment); } } function withdraw(uint256 account) public { require(now >= balances先来分析合约中的存在转账功能函数withdraw()和confiscate():
function withdraw(uint256 account) public { require(now >= balances
继续来看第二个函数confiscate():
function confiscate(uint256 account, bytes16 _secret) public { require(msg.sender == owner); require(secret == _secret); require(now >= balances
如果要使用confiscate()函数进行转账,我们需要解决该函数中的前三行代码判断条件,由于题目给出了提示-存储溢出以覆盖银行所有者。我们继续分析该合约变量覆盖问题。
从合约源码可看到,该合约中有四个全局变量(balances,head,owner,secret),在solidity中,全局变量存储在storage当中,对于复杂的数据类型,比如array(数组)和struct(结构体),在函数中作为局部变量时,也会默认储存在storage当中。并且solidity的状态变量存储时,都是按照状态在合约中的先后顺序进行依次存储。
也就是说目前合约的四个全局变量存储如下:
storage<0> : balancesstorage<1> : headstorage<2> : ownerstorage<3> : secret在invest()函数中,通过Investment storage investment = balances
investment.amount = msg.value; investment.deposit_term = deposit_term;investment.owner = msg.sender;在函数中作为局部变量时,也会默认储存在storage当中,由于结构体变量investment并未对三个成员进行初始化,所以当变量存储时依然会按照顺序存储在storage<0,1,2>中,那么目前storage中的存储为数据为:
这里局部变量覆盖全局变量时要特别注意一点:局部变量amount覆盖全局变量balances时,由于balances变量是数组的长度(目前数组中有合约部署者传入的一组数据,故balances为1),当其他调用者也传入的一组数据,比如传入的msg.value值为1(也就是amount的值为1时),之后变量覆盖后的balances值也为1,但是由于传入了一组数据后,数组的长度balances变为2,由于变量覆盖相互影响的关系,balances的值为2后,amount的值也变为2,也就是说虽然传入的msg.value值为1,但最终amount的值为2。
我们继续来分析合约漏洞,由于上图invest()函数中赋值的三个变量(msg.value,deposit_term,msg.sender)都可控:
第一个变量msg.value,是调用者传入的资金;
第二个变量deposit_term本身的含义是存款期限,调用者可根据自己情况输入需要存款的时间,在合约中发生变量覆盖后则代表head值(存款数据的索引)。并且在invest()函数中存在和deposit_term变量相关联的判断条件require(deposit_term >= balances
由于我们最终需要利用confiscate()函数中的transfer函数进行转账,如果按照正常逻辑运算,我们存钱后至少需要一年时间才能取出,所以该判断条件(require(now >= balances
可以看到这两行代码的条件判断中加减操作并没有做安全防护,这里假如我们使balances
第三个变量msg.sender,从上图可以看出,该变量传入后覆盖全局变量owner,当前调用者地址就会变为合约所有者,从而就可绕过confiscate()函数中msg.sender == owner判断条件。
由于secret存储在storage(storage变量是指永久存储在区块链中的变量),所以我们可以调用storage索引获取里面的值。
至此confiscate()函数中的前三句判断条件均已满足。
解题思路通过分析BelluminarBank合约漏洞,我们可以利用整数溢出,变量覆盖,访问权限等漏洞攻击转出合约所有余额。具体解题思路如下:
通过调用invest()函数传入account为1,deposit_term为115792089237316195423570985008687907853269984665640564039457584007913098103936,携带的msg.value为1wei。account始终根据第一句的判断条件进行赋值。msg.value赋值amount,再进行balances变量覆盖(由于变量循环赋值的关系),最终结果balances=amount=2;deposit_term值变量覆盖也成为head值;调用者地址msg.sender最终变量覆盖后会成为合约所有者owner。调用之后balances
攻击演示本次攻击演示在ropsten测试网进行,使用工具为Remix+Matemask+myetherwallet
Remix在线编辑器:http://remix.ethereum.org/
MetaMask钱包插件:https://metamask.io/
MyEtherWallet在线钱包:https://www.myetherwallet.com/
1.首先部署BelluminarBank漏洞合约使用在线编辑器Remix通过Meta Mask在线钱包A地址部署BelluminarBank合约,部署时给合约传入参数为:
value:31337 wei,deposit_term:0x00000000000000000000000000000001,_secret:1000
(为了方便查看数据,我们将合约源码中的一部分内容进行了可见性修改)
部署完成后目前合约中变量值为以下
2.使用myetherwallet在线钱包调用BelluminarBank合约在remix中获取api并复制部署的合约地址,填入myetherwallet钱包中。
连接成功
3.调用invest()函数修改合约所有者owner,存款期限数值deposit_term,变量重复覆盖值amount传入参数为:
value:0.000000000000000001 ETH,account:1,deposit_term:115792089237316195423570985008687907853269984665640564039457584007913098103936。
完成后目前合约中变量值为以下
4.调用invest()函数修改存款期限数值deposit_term(修改head为0),变量重复覆盖值amount传入参数为:
value:0.000000000000000002 ETH,account:2,deposit_term:0。
完成后目前合约中变量值为以下
虽然上图中显示的合约全部余额为31340 wei,但调用过程中出现循环变量覆盖,导致数组中的余额为31337+2+3 =31342 wei,如下图所示:
为了使合约本身余额与数组中的amount匹配,这里我们选择强制给该转币。
5.通过c地址部署合约并调用taijie()函数自毁合约给BelluminarBank合约转2 wei,平衡合约数组中的余额。
自毁成功后,BelluminarBank合约余额变为31342 wei。
6.调用confiscate()函数最终取走合约所有的余额传入两个参数:account:2,secret:0x00000000000000000000000000000001
调用完成后,BelluminarBank合约余额变为0。
至此完成攻击演示
总结本篇文章中,我们通过2018WCTF比赛中的BelluminarBank智能合约题目,详细分析了合约存在的漏洞问题,提供了解题思路并进行了攻击演示,其中使用的相关工具已在文中给出链接,希望对智能合约初学者及爱好者有所帮助,下一篇我们会继续分享CTF智能合约经典题目,请大家持续关注。