PolySwarm Smart Contract Hacking Challenge Writeup

( Original text by raz0r )

This is a walk through for the smart contract hacking challenge organized by PolySwarm for CODE BLUE conference held in Japan on November 01–02. Although the challenge was supposed to be held on-site for whitelisted addresses only, Ben Schmidt of PolySwarm kindly shared a wallet so that I could participate in the challenge.

The target smart contract called 

CashMoney

 featured a set of honeypot tricks that were described in Ben’s talk “Smart Contract Honeypots”. A naïve attacker would call 

do_guess()

function with a number that was compared with a private variable 

current

, which could easily be revealed off-chain. However, the condition would never work as expected because of the following code:

1
2
3
4
Guess storage guess;
guess.playerNo = players[msg.sender].playerNo;
guess.time = now;
guesses.push(guess);

Solidity prior to 0.5.0 allows uninitialized storage pointers. Since the contract was compiled with solc 0.4.25, the code above would silently overwrite the first element in contract’s storage with 

players[msg.sender].playerNo

 value. As a result, variable 

current

 which is the first in the storage would be changed right before the comparison. In order to make the condition pass, one had to call 

do_guess()

 with their player number instead of 

current

 value. The contract allowed to update one’s number and name via 

updateSelf()

, which was quite handy since 

do_guess()

 had a restriction on the number range (only 0–10).

After the check succeeded it seemed that nothing could stop me to receive the prize. However, it was just the beginning of the long journey into EVM bytecode, as 

do_guess()

 reverted for unknown reason. After a short debugging session in Remix it was clear that the following line caused it:

1
2
// you win!
winnerLog.logWinner(msg.sender, players[msg.sender].playerNo, players[msg.sender].name);
CashMoney

 address on EtherScan revealed the source code of another contract called 

WinnerLog

, however 

winnerLog

 variable pointed to some other contract which had no verified source code. An attempt to verify the source code was unsuccessful which proved the assumption that 

WinnerLog

 had some different logic inside. A quick recon on 

WinnerLog

revealed a magic string 

dogecointothemoonlambosoondudes!

 in contract’s storage. The first idea was of course to use this string as a username, however 

do_guess()

 still reverted. Other variations like reversing this string or using capital letters also failed. As quick attempts happened to be ineffective, I moved to reverse engineering the contract’s bytecode. Although decompiled contract was hardly comprehensible, EtherVM provided some hints on the logic inside 

logWinner()

 function.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
} else if (var0 == 0x7fd4b61a) { // logWinner()
/**/
if (msg.sender != storage[0x03] & 0x02 ** 0xa0 - 0x01) { revert(memory[0x00:0x00]); }
/**/
} else {
/**/
label_03F5:
    
var9 = var6; // var6 is magic string
    
var10 = var7 & 0x1f;
    
if (var10 >= 0x20) { assert(); }
    
var9 = byte(var9, var10) * 0x02 ** 0xf8 ~ 0x02 ** 0xf8 * 0x42;
    
var10 = var5;
    
var11 = var7 & 0xffffffff;
    
if (var11 >= memory[var10:var10 + 0x20]) { assert(); }
    
var temp35 = var10 + 0x20 + var11;
    
memory[temp35:temp35 + 0x01] = byte((memory[temp35:temp35 + 0x20] / 0x02 ** 0xf8 * 0x02 ** 0xf8 ~ var9) & ~0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff, 0x00);
    
var8 = var8;
    
var7 = var7 + 0x01;
    
if (var7 & 0xffffffff >= memory[var5:var5 + 0x20]) { goto label_0475; }
    
else { goto label_03F5; }
}

It was quite obvious that the function could be called only by 

CashMoney

 contract, and some operations were done on each byte of the magic string. Therefore I had to find such username that would satisfy some obscure conditions inside a closed-source smart contract code. Sounds like a perfect task for symbolic execution.

Manticore is a symbolic execution tool created by Trail of Bits which supports Ethereum Virtual Machine. It proved to be effective at CTFs, so I decided to give it a try.

Firstly, it was necessary to set up addresses and create the 

WinnerLog

 smart contract:

1
2
3
4
5
6
7
import
binascii
from
manticore.ethereum
import
ManticoreEVM, ABI
m
=
ManticoreEVM()
owner_account     
=
m.create_account(balance
=
1000
, name
=
'owner'
,     address
=
0xbc7ddd20d5bceb395290fd7ce3a9da8d8b485559
)
attacker_account  
=
m.create_account(balance
=
1000
, name
=
'attacker'
,  address
=
0x762C808237A69d786A85E8784Db8c143EB70B2fB
)
cashmoney_contract
=
m.create_account(balance
=
1000
, name
=
'CashMoney'
, address
=
0x64ba926175bc69ba757ef53a6d5ef616889c9999
)
winnerlog_contract
=
m.create_contract(init
=
bytecode, owner
=
owner_account, name
=
"WinnerLog"
, address
=
0x2e4d2a597a2fcbdf6cc55eb5c973e76aa19ac410
)

After that, the smart contract state had to be recreated. There was just one transaction on the mainnet, supposedly to allow 

CashMoney

 contract call 

WinnerLog

:

1
2
m.transaction(caller
=
owner_account, address
=
winnerlog_contract,
data
=
binascii.unhexlify(b
"c3e8512400000000000000000000000064ba926175bc69ba757ef53a6d5ef616889c9999"
), value
=
0
)

The next step was to create a symbolic buffer and send a transaction to call 

logWinner()

 with that symbolic buffer:

1
2
3
symbolic_data
=
m.make_symbolic_buffer(
64
)
calldata
=
ABI.function_call(
'logWinner(address,uint256,bytes)'
, attacker_account,
0
, symbolic_data)
m.transaction(caller
=
cashmoney_contract, address
=
winnerlog_contract, data
=
calldata, value
=
0
, gas
=
10000000
)

And finally my goal was to find at least a single running state, i.e. the one that finished with a 

STOP

 instead of 

REVERT

 or 

THROW

:

1
2
3
4
5
for
state
in
m.running_states:
    
world 
=
state.platform
    
result
=
state.solve_one(symbolic_data)
    
print
(
"[+] FOUND: {}"
.
format
(binascii.hexlify(result)))
    
break

After several minutes manticore successfully found a username that would not result in a reverted transaction:


After setting this sequence of bytes as my username, I successfully claimed one of the prizes. The complete solution can be found on GitHub.

РубрикиБез рубрики

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *