时间:2023-06-17|浏览:200
用戶喜愛的交易所
已有账号登陆后会弹出下载
近年来,各大型CTF比赛中都有了区块链攻防的身影,其中绝大多数出现的题目都是区块链智能合约攻防。本系列文章主要以智能合约攻防为中心,来剖析智能合约攻防的要点。前两篇我们分享了合约反编译和反汇编的基础内容。接下来我们会继续分享CTF比赛中智能合约常见题型(如重入,整数溢出,空投和随机数可控等)及解题思路,相信会给读者带来不一样的收获。
本篇文章主要以2020年NSSCCTF上的skybank题目为例,分享智能合约薅羊毛的题型。该题型也是多次出现在CTF比赛中。相对于之前的系列文章内容,本篇薅羊毛题型更容易理解,比较简单易懂。
题目分析:
题目提示:原始合约的opcode需进行反编译;空投及最终判断函数分别为gether和ObtainFlag;触发ObtainFlag函数事件event则攻击成功;需给合约提供资金。
查看合约题目,合约存在0.62 ether,没有给出合约源码。
由于拿到题目后只有合约的opcode,所以需要进行逆向。这里我们推荐OnlineSolidityDecompiler在线网站(https://ethervm.io/decompile)进行逆向,具体源码还原过程请参考系列文章反编译篇和反汇编篇。
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)会判断调用者地址的余额是否大于等于1000000000wei,如果满足该条件,就执行emit sendflag(base64email, md5namectf)代码。从题目可以得出,只要参赛者触发sendflag事件并将参数输出表示获取flag成功。
由于参赛者初始调用题目合约skybank时,调用地址在所属合约的资金为0。因此,我们需要通过合约逻辑获取资金。接下来我们来看获取空投函数gether:
function gether() public { require(balances[msg.sender] == 0); balances[msg.sender] += 10000000; }
在geth函数中,第一句代码require(balances[msg.sender] == 0)判断当前调用者的地址是否为0。如果满足条件,就给该调用者加10000000 wei的资金。在我们最终触发sendflag事件的ObtainFlag函数中,需要1000000000wei。因此,只要调用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,也就是说我们可以重复该函数进行转账操作。
解题思路:
通过以上分析,可以总结出两种解题思路:
第一种:
- 通过A地址调用gether函数获取空投 - 调用Transfer函数将A地址余额转至B地址 -
- 使用多个地址调用gether获取空投 - 将获取空投汇聚至固定地址 - 通过该固定地址调用ObtainFlag并触发事件
攻击演示:
我们来演示一下第一种解题思路的攻击过程,使用Remix+MetaMask对攻击合约进行部署调用。
- 自毁合约给题目合约转币
由于题目合约的初始状态没有ether,因此我们可以通过自毁函数,强行将ether转入题目合约地址。虽然当前题目合约有一定资金,我们为了演示的完整性,也演示一次自毁。
pragma solidity ^0.4.24;
contract burn { function kill() public payable { selfdestruct(address(0xE6BEBc078Bf01C06D80b39E0bb654F70C7B0C273)); } }
部署burn合约,并利用kill函数带入0.02Ether进行自毁,将Ether发送到题目合约地址。
- 使用A地址部署最终调用者合约attacker2
调用代码:
pragma solidity ^0.4.24;
interface skybankInterface { function ObtainFlag(string base64email, string md5namectf); }
contract attacker2 {