前言
- 前两天设计了一个区块链的题目,其中出现了很多问题,还好在比赛第一天夜里修复了问题,在这里简单记录一下,给各位师傅带来了麻烦,表示歉意(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; |