Hackergame2019 JCBank
- 以太坊Kovan测试链
- 合约地址:https://kovan.etherscan.io/address/0xE575c9abD35Fa94F1949f7d559056bB66FddEB51
- 题目源码:https://github.com/hitcxy/challenges/tree/master/2019/Hackergame2019_JCBank
题目的源码是直接给出的
1 | /** |
有两个get_flag
函数,先看get_flag_1
函数,要求传进去的参数guess
与secret
相等。注意到合约开始的地方定义了一个属性为public的constructor
(构造器),这样的话我们就可以直接通过读取storage
来获得变量secret
的值
读取secret
变量的值有两种方法:
在etherscan的contract页面已经识别到了
Constructor Arguments
因为constructor中只有一个参数
init_secret
,然后把init_secret
的值赋给了secret
,所以000000000000000000000000000000000175bddc0da1bd47369c47861f48c8ac
就是secret
的值可以通过Truffle控制台来读取链上的Storage
然而调用get_flag_2
需要我们的余额大于1000000000000
eth,因为这个合约只有四个函数,所以很容易就注意到withdraw
函数是有问题的:因为函数中使用了<address>.call.value()
这个函数,在rickgray师傅的这篇博客中也提到了,.call.value()
函数不能有效防止重入
所以这里就存在重入漏洞了(重入漏洞原理可以看这里的第十一关)
在 withdraw
函数的 msg.sender.call.value(amount)();
一句代码中,合约会给调用者的地址上转账 amount
金额的币,但这里没有使用有 gas 限制的 msg.sender.transfer
或者 msg.sender.send
来转账,所以如果转账的目标是一个合约,合约接收到转账,它的 fallback 函数会被运行。其实在编译题目的合约时,编译器在这里会提示一个 warning。
在这一步中,我们可以自己写一个新的合约,这个合约的 fallback 函数是 payable 的,然后当它被执行时,我们在fallback函数中再次调用题目合约的 withdraw
函数。此时,balance
还没有被减去第一次转账的金额,所以转账可以再次发生。我们可以使用一个计数器让转账只会发生两次,然后 balance
也会被减去两次。如果第二次减法不够减,就会发生整数溢出。因为 balance
是无符号的,此时你的合约对应的 balance
会变成一个巨大的数,就可以去拿 flag 了。
如果题目合约中以太币数量为 0,那么第二次转账会失败,所以需要提前用另一个地址给题目合约充一些币。
exp:
1 | contract Hack { |
Bytectf bet
题目地址:0x30d0a604d8c90064a0a3ca4beeea177eff3e9bcd@ropsten
附件:bet.broken:
1 | ...... |
考点:整数下溢
由于刚学合约不久,如果有说错的地方还希望师傅们帮忙指正:D
题目没有给出源码,所以需要我们自己逆了。我比较习惯etherscan的decompile和ethervm.io的decompile对照着看,所以先把两边都反编译出来
分析合约功能
可以看到合约中有三组映射和两个未知的全局变量,只有第一个映射balanceOf
被识别出来了
再看ethervm.io的decompile,被识别出来的属性为public的函数有这么几个:空投函数profit
,获取flag函数payforflag
,查询余额函数balanceOf
,Bet
函数,gift
函数,存钱函数deposit
和其余未识别的函数
可以看到调用payforflag
函数需要balanceOf[msg.sender]
大于100000(0x186a0)
接下来就逐个分析函数的功能了
1.先看profit
函数,profit
函数需要mapping2[msg.sender]==0
,现在我们不知道mapping2代表什么,先不管它,而从ethervm.io的decompile可以看到mapping2对应的是0x03
标志位。
画框的if判断对应的就是require,而balanceOf对应的是0x02
标志位。profit
函数的功能就是在满足条件时使调用函数的人的余额+1,这时候就可以看出mapping2
代表的应该是看我们是否调用过profit
函数,如果调用过的话就把0x03
标志位(mapping2)置为1,不允许再次调用
2.接着再看func_03F7
函数(即ad17b493
函数)和Bet
函数
在Bet
函数中,是把msg.sender
复制给var2
(后面的或计算和与计算可以直接忽略,因为计算的结果还是msg.sender
)。同样现在我们还不知道var2
的含义,但是不要紧,只要先调用Bet函数将msg.sender
赋值给var2
就可以保证我们调用func_03F7
函数(即ad17b493
函数)的时候可以通过第一个require了
继续分析func_03F7
函数(即ad17b493
函数),还需要balanceOf[msg.sender] - 2 > 0
和mapping3[msg.sender] == 1
,mapping3
也不知道代表什么,所以继续去ethervm.io分析
第三个require对应的是第三个if判断,可以看到在第三个if判断中是判断0x04
标志位是否为1,0x04
标志位如果不为1就直接revert,为1的话才继续向下执行。接下来就是余额减2,如果函数的参数_arg0和全局变量var1不相等的话,就revert;如果相等的话,就再给余额加2.
3.最后分析剩下的几个函数func_0219
函数(即1727bb94
函数)、func_04E4
函数(即f98b23c9
函数)和deposit
函数
func_0219
函数(即1727bb94
函数):这个函数功能与
func_03F7
函数很像,调用函数需要balanceOf[msg.sender] > 0
,先将余额减1,如果这个函数的参数_arg0
和var1
变量相等的话,就进入else,使得余额加2,最后将mapping2[msg.sender]
置为1。到这里出了个问题,还记得在profit
函数中的mapping2吗,对应的是0x03
标志位,用来验证我们是否已经调用过空投函数,而这里却也是mapping2感觉有问题。再去看ethervm.io,发现ethervm.io这里是0x04
标志位,这样的话就说得通了。0x02
标志位对应balanceOf,0x03
标志位对应是否调用过profit
。0x04
标志位的含义还不清楚,但在func_03F7
函数的调用中需要0x04
标志位为1func_04E4
函数(即f98b23c9
函数):在这个函数中,require跟之前的一样,只要先调用
Bet
函数使我们的msg.sender
复制给全局变量var2
就可以通过这个require;下面会把这个函数的参数赋值给var1
,而这个参数是我们可控的,成功调用这个函数后就可以使func_03F7
函数和func_0219
函数进入if判断了deposit
函数:就是一个最普通的存钱函数,
de0b6b3a7640000
是十六进制数。
Attack
分析完函数功能后就要找漏洞点了,因为调用payforflag
需要余额大于100000显然不现实。我们注意到func_03F7
函数(即ad17b493
函数)和func_0219
函数(即1727bb94
函数)都有一个减去余额的操作可能造成下溢。明确漏洞点后就可以构思一下攻击链了:
- 先调用
Bet
函数使得将我们的msg.sender
赋值给var2; - 调用
func_04E4
函数(即f98b23c9
函数),使var1变为0(任意数字都可); - 调用
profit
空投函数使余额变为1; - 调用
func_0219
函数(即1727bb94
函数),刚才已经知道var1为0,所以这里也将函数参数设为0,使我们的余额变为2,同时将mapping3[msg.sender]
(即0x04
标志位)置为1; - 再次调用
func_0219
函数(即1727bb94
函数),为了达到余额减1的目的,故意将参数设为0以外的数字,使得余额变为1; - 调用
func_03F7
函数(即ad17b493
函数),三个require都已满足(对于第二个require,现在余额只有1,减2的话也会下溢,所以也会满足),就可以执行余额减2的操作从而实现溢出; - 调用
payforflag
函数拿flag。