COINFLIP
根据题目描述,没有附件,直接看源码:https://ropsten.etherscan.io/address/0xf60adef7812214ebc746309ccb590a5dbd70fc21#code
1.要获取flag,必须账户余额必须大于500ether(注意:这里的ether只是一个单位,10的18次方):
1 | function CaptureTheFlag(string b64email) public returns(bool){ |
2.看代码如何获取token,该处有一个空投函数:
1 | function Ap() public { |
当账户余额是0时,调用该函数能够获取1ether,那么如果一个账户要有500ether,就有两种方法
创建500个账户,分别调用一次该函数,并将获取的token都转到同一个账户X,那么X账户余额就有了500ether,就可以执行CaptureTheFlag。
创建一个账户,每调用Ap()一次后将余额转到账户X,再继续调用Ap(),再转账,执行500次。
3.攻击合约:
1 | contract Hack |
但由于gas的限制,循环次数可能不能直接设置500,我这里设置200,200,100.
执行结束后,我的账户里就有了500ether,这时候就可以成功调用CaptureTheFlag,获得flag。
flag:RoarCTF{wm-da64ccd4c370ab6a8ee64381cfa14a1e
合约第二题 Honey Lock
题目给了源码(后来发现是部分)和合约地址:https://ropsten.etherscan.io/address/0x8d73365bb00a9a1a06100fdfdc22fd8a61cfff93#code
第一次做比赛题,开始被坑了,以为附件给出的就是全部源码。下面给出解题过程:
读附件源码,调用CaptureTheFlag,必须满足两个条件:
takeRecord[msg.sender] == true;
balances[msg.sender] == 0
1 | function CaptureTheFlag(string b64email) public returns(bool){ |
第一个条件有两个地方可以得到满足:构造函数HoneyLock()和takeMoney(),显然是要调takeMoney(),但是调用该函数后会得到一个空投,balances[owner] = airDrop;
,从而无法满足上述的条件2,那么就要找找怎么把账户的余额转出去。看附件代码,有一个transfer()和withdraw() 都可以实现,但是仔细看一下,发现了问题:
对于transfer(),加了一个时间转账的条件:modifier lock() { require (now > timeHouse[msg.sender]); _; }
现在的时间必须大于timeHouse[msg.sender]
,但是在调用takeMoney()的时候,该变量的时间已经被赋值为一年后:timeHouse[owner] = time;
,uint public time = now + 1 years;
,因此该函数没法调用。
对于withdraw() ,要执行成功必须满足两个条件:onlyOwner,和takeRecord[msg.sender] == true,这里第二个条件在调用了takeMoney()之后就已经满足了,主要是第一个,看了一个代码,发现:
1 | function useCode(uint256 code) public payable { |
猜数字,我们知道在区块链里全局变量是可以直接通过getStorage读取的,只要知道变量对应的存储slot就可以,分析一下知道guessCode的slot=5,guessValue的slot=6,用rpc api查看值:
code=0x29a(对应十进制666),value=0x2386f26fc10000(对应十进制0.01ether),调用useCode,666作为参数,msg.value=0.01ether就可以调用成功,此时 owner 就是 msg.sender了,按道理应该是可以调用withdraw(),可是调用报错。
只能改变思路,想起来还有一个transferFrom()也可以转账,这边也没有重写,应该可以通过approve和transferFrom()来转账。操作一波后发现也调用不了。百思不解不得其解,不过就在刚刚写文档的时候想起来了,transferFrom()内部也会调用transfer()啊。
这也不行,就想起了给的合约地址,没有源码,就反编译了一波,发现了原来给的不是全部源码呀,而且还有误导的地方,比如onlyOwner这里并不是要owner=msg.sender,而是(msg.sender == storage[4])(这个onlyOwner不知道是不是自己理解有问题,还需要继续看一下)。然后源码里面要有另一个无法反编译出函数名的函数,和transferFrom()有点类似,但是多了一个参数,这个参数对该转账加了一个限制条件:
1 | //https://ropsten.etherscan.io反编译的伪代码 |
限制条件就是storage[2] + msg.sender == _arg3,storage[2]的内容对应十进制是53231323(出题人莫不是在玩吉他的时候想出的题?),也就是说第四个参数是msg.sende加上53231323。
调用5ad0ae39() 之前需要调用approve(),把账户A的余额委托给B,然后由B调用5ad0ae39()将A的钱转给C,简单一点,就将账户A余额委托给A,然后A调用5ad0ae39()将钱转给地址0x00:
这个approve()的调用可以直接在remix里面进行:
5ad0ae39()需要用api,先把data整理一下:函数名+四个参数:
0x5ad0ae39000000000000000000000000967f8ac6502ecba2635d9e4eea2f65ad4940b1b1000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000003e8000000000000000000000000967f8ac6502ecba2635d9e4eea2f65ad4c6cf08c
然后在console里调用eth.sendTransaction():
metamask会弹出转账窗口,调整gas数就等待交易完成了。
这边开始用的sendSignedTransaction(),但始终不成功,不知道有没有大佬熟悉这个接口调用的,指导一下。
到这里就满足takeRecord[msg.sender] == true并且账户余额为0,可以调用CaptureTheFlag方法,坐等flag。
flag:RoarCTF{wm-87fc255216991be9173a59aa8b6845a0}