前言
- 前两天设计了一个区块链的题目,其中出现了很多问题,还好在比赛第一天夜里修复了问题,在这里简单记录一下,给各位师傅带来了麻烦,表示歉意(emmm),下面先说明一下每个版本都修复了什么问题- 第一个版本我就是头脑发热,把题目设计成 1000 eth就能拿到flag,我真是弟弟行为,还好及时下线
- 第二个版本是任意地址写条件没有控制的很苛刻,导致天枢利用了这一点,在非预期做出题目之后,把 codex的length给修改成了一个相对小的数值,造成其他队伍没法做题,这一点被有心之人利用了,他们写了个脚本一直攻击刚部署上的合约,修改数组长度(23333,硬生生被玩成了AD)
- 第三个版本是修复了版本二的问题,应该是可以正常做题的
- 后来仔细思考了一下,版本三还是有一些问题的,不过选手做题的时候没有遇到,但是担心会出问题,所以就有了最终版本四(其实版本四也有一些问题,在 buy()中有一条require(storage[0x02]==1)限制,虽然在payforflag后会回到初始化状态,但是这里头铁使用了storage变量,导致一个问题是如果正在解题的队伍使这个条件成立了,恰巧另外一支队伍也正在解题,那么他们就可以乘顺风车,如果这里使用memory变量就好了
- 变更了版本其实主要还是想要让题目按照预期进行求解,给各个队伍造成了麻烦,表示抱歉(2333333…..),下面介绍一下题目
 
- 第一个版本我就是头脑发热,把题目设计成 
- 以太坊 Ropsten测试链
- 合约地址:https://ropsten.etherscan.io/address/0x168892cb672a747f193eb4aca7b964bfb0aa6476
- 题目:https://github.com/hitcxy/blockchain-challenges/tree/master/2019/xctf_final/Happy_DOuble_Eleven
EVM 逆向
- 先进行合约逆向,使用 https://ethervm.io/decompile
- 可以逆向出下面几个关键 function
0x6bc344bc payforflag(string)
- 要求 msg.sender == storage[0x00]
- 要求 msg.sender后12位为0x111
- 要求 storage[0x06] == 0x03
- 要求 storage[0x05] > 0x8ac7230489e80000
| 1 | function payforflag(var arg0) { | 
0xed21248c Deposit()
- 每次 msg.value >= 0x1b1ae4d6e2ef500000,即msg.value >= 500 eth,然后storage[0x05] += 1
- 结合 payforflag来看,这个操作不现实,因为payforflag中要求storage[0x05] > 0x8ac7230489e80000,即要将msg.value >= 500 eth进行0x8ac7230489e80000+1次
| 1 | function Deposit() { | 
0x24b04905 gift()
- 要求 address(msg.sender).code.length == 0,即在合约constructor中运行即可
- 要求 msg.sender后12位为0x0111
- 满足上述条件后,storage[0x04] = 100,storage[0x05] += 1,storage[0x06] += 1
| 1 | function gift() { | 
0x23de8635 func_06CE(arg0)
- 这里是调用了 0xa8286aca的function
- 总体来看,这里调用了 0xa8286aca两次,输入同样的参数arg0一次,0xa8286aca第一次和第二次返回的结果不一样,但是一个function当它的参数确定时,他的返回结果也应该是确定的,而不会两次不一样,所以0xa8286aca这里应该是一个接口函数,我们是可以改写的,最后改变了storage[0x02]的值
| 1 | function func_06CE(var arg0) { | 
0x9189fec1 guess(uint256)
- 要求 arg0 == block.blockHash(block.number - 0x01) % 3,这个很容易满足,因为利用区块号生成的随机数是可预测的
- 满足要求后,storage[0x00] = (storage[0x00] & ~(0xff * 0x0100 ** 0x14)) | 0x0100 ** 0x14,即storage[0x00]的高96位数值为1
| 1 | function guess(var arg0) { | 
0xa6f2ae3a buy()
- 要求 storage[0x06] == 1,这些调用gift()空投可以完成
- 要求 storage[0x05] == 1,这些调用gift()空投可以完成
- 要求 storage[02] == 1,结合func_06CE来看,只需使得0xa8286aca第二次调用返回1即可
- 要求 storage[0x00] / 0x0100 ** 0x14 & 0xff == 1,即storage[0x00]的高96位数值要求为1,这个满足guess即可
- 满足上述要求后,storage[0x05] += 1,storage[0x06] += 1
| 1 | function buy() { | 
0x47f57b32 retract()
- 要求 storage[0x01] == 0
- 要求 storage[0x05] == 0x02,调用gift后,再调用buy即可
- 要求 storage[0x06] == 0x02,调用gift后,再调用buy即可
- 要求 storage[0x00] / 0x0100 ** 0x14 & 0xff == 0x01,即storage[0x00]的高96位数值要求为1,这个满足guess即可
- 满足上述要求之后,storage[0x01] -= 0x1,这里应该是修改数组的长度
| 1 | function retract() { | 
0x0339f300 revise(uint256,bytes32)
- 要求 storage[0x01] >= 0xfffffffffffffffffffffffffffffffffffffffffffffffffffffffffff00000,经过retract()后即可满足
- 要求 storage[0x05] == 0x02,
- 要求 storage[0x06] == 0x02,
- 要求 storage[0x00] / 0x0100 ** 0x14 & 0xff == 0x01,即storage[0x00]的高96位数值要求为1,这个满足guess即可
- 要求 arg0 >= storage[0x01]
- 满足上述要求后,后面进行了 storage写操作,这里是任意写操作
| 1 | function revise(var arg0, var arg1) { | 
0xa9059cbb transfer(address,uint256)
- 这里是进行 storage[0x04] 之间的转账操作
| 1 | function transfer(var arg0, var arg1) returns (var r0) { | 
0x2e1a7d4d withdraw(uint256)
- 要求 storage[0x05] == 0x02
- 要求 storage[0x06] == 0x03
- 要求退款每次 < 100
- 要求 storage[0x04] < arg0,即余额比每次退款要多
- 要求合约余额比退款要多
- 满足条件后,storage[0x04] -= arg0,然后调用call函数进行转账(这里存在重入攻击,因为没有对gas做控制),最后storage[0x05] -= 0x01
| 1 | function withdraw(var arg0) { | 
分析
- 通过上面的分析后,这样整个攻击链就出来了- 生成符合要求的外部账户,在 constructor中调用gift()
- 调用 0x23de8635 func_06CE,这里要利用bytecode的方式部署,因为我们不知道func_06CE中调用的接口函数0xa8286aca的函数名,所以利用bytecode的方式部署第三方合约,将fake(uint256)对应的函数选择id改为0xa8286aca即可,这样调用0xa8286aca就是调用我们重写之后的0xa8286aca了,用bytecode部署可以用在线的 myetherwallet.com
- 调用 guess(),然后调用buy()
- 调用 retract()和revise()修改owner
- 部署第三方子合约,第三方子合约调用 gift()和transfer()给攻击合约转账,然后调用withdraw()进行重入攻击
- 最后调用 payforflag即可
 
- 生成符合要求的外部账户,在 
exp
- 外部账户满足其部署的第一个合约地址最后 12位是0x111
- 可以用下述脚本生成,generate_eoa1()是生成外部账户最后12位为0x111,generate_eoa2()是生成满足外部账户部署的第一个合约最后12位是0x111,我们用generate_eoa2()即可
| 1 | from ethereum import utils | 
- exp如下
| 1 | pragma solidity ^0.4.23; | 
Source
- 最后附上合约源代码及其已知源代码的 exp
| 1 | pragma solidity ^0.4.23; | 

 
          