Paradigm CTF-LOCKBOX

  • bixia1994
  • 更新于 2021-07-08 23:09
  • 阅读 2661

这是Paradigm CTF的一道题,:laughing:这道题考察的知识点比较多,涉及到ABI编码,RLP编码,黄皮书中附录F等。可以看作是一个编码相关的大集合题目。

Paradigm CTF-LOCKBOX

这是Paradigm CTF的一道题,整体来说难度不算特别大,与之前的STATIC CALL难度相似。:laughing:这道题考察的知识点比较多,涉及到ABI编码,EIP-712等。可以看作是一个编码相关的大集合题目。

同样本文的参考连接如下:Paradigm CTF solutions (github.com)

目前作者正在找智能合约相关的工作,希望能跟行业内人士多聊聊 🐟 。如果你觉得我写的还不错,可以加我的微信:woodward1993

题目分析:

contract Setup {
    Entrypoint public entrypoint;

    constructor() public {
        entrypoint = new Entrypoint();
    }

    function isSolved() public view returns (bool) {
        return entrypoint.solved();
    }
}

可以看到该题的设置很简单,就是创建一个Entrypoint合约,最后判断该合约是否solved。下面我们分析下目标合约:

:one: Stage 合约

该合约是后续合约的母合约,一共干了三件事:1. 定义了一个全局变量address, 2. 定义一个函数getSelector(), 3. 定义了一个modifier。比较有意思的是这个modifier, 我们将逐行分析下该modifier. 该Modifier的作用是调用全局变量存储的地址上的solve方法。

modifier _(){
    _;
    assembly{
    //第一步:拿到全局变量Stage, 判断如果stage的值没有更新则返回
    let stage := sload(0x00)
    if iszero(stage) {
        return(0,0)
    }
    //第二步:调用Stage上的getSelector()函数,将结果存储在内存中
    // keccak(abi.encode("getSelector"))[0:0x04] = 0x034899bc
    mstore(0x00, 0x034899bc00000000000000000000000000000000000000000000000000000000)
    switch call(gas(),stage, 0, 0x00, 0x04, 0x00, 0x04)
    case 0x00 {
        revert(0, returndatasize())
    }
    case 0x01 {
        returndatacopy(0x00,0x00,0x04)
    }
    //第三步:调用Stage合约,函数选择器为getSelector()函数的返回值,参数为CALLDATA[0x04:]
    calldatacopy(0x04, 0x04, sub(calldatasize(),0x04))
    switch call(gas(), stage, 0, 0x00, calldatasize(), 0x00, 0x00)
    //第四步:如果调用失败,则REVERT
    case 0x00 {
        returndatacopy(0x00,0x00,returndatasize())
        revert(0, returndatasize())
    }
    case 0x01 {
        returndatacopy(0x00,0x00,returndatasize())
        return(0, returndatasize())
    }
    }
}

:two: EntryPoint合约

bool public solved;

function solve(bytes4 guess) public _ {
    require(guess == bytes4(blockhash(block.number - 1)), "do you feel lucky?");
    solved = true;
}

对于EntryPoint合约,它的要求很简单:即传入的参数guess 满足 guess == bytes4(blockhash(block.number - 1)即可。

故此时我们需要传入的参数为:

bytes memory data = abi.encode(
        Entrypoint.solve.selector,
        bytes4(blockhash(block.number - 1)) // => bytes4 guess
)

:three:Stage1 合约

function solve(uint8 v, bytes32 r, bytes32 s) public _ {
        require(ecrecover(keccak256("stage1"), v, r, s) == 0x7E5F4552091A69125d5DfCb7b8C2659029395Bdf, "who are you?");
}

由于modifier的作用,我们传入的同一个参数会依次被EntryPoint, stage1, stage2, stage3, stage4, stage5调用,故我们需要让该参数依次满足所有的要求。Stage1的要求是传入3个参数,让其满足ecrecover(keccak256("stage1"), v, r, s) == 0x7E5F4552091A69125d5DfCb7b8C2659029395Bdf. 首先我们需要理解ecrecover函数的作用

查阅黄皮书得知:ECDSARECOVER函数是与ETH的交易签名相关。它假设发送方有一个有效的私钥,它是一个随机选择的正整数(以大数形式表示为长度为32bytes的字节数组)

$$ \mathtt{ECDSAPUBKEY}(p{\mathrm{r}} \in \mathbb{B}{32}) \equiv p{\mathrm{u}} \in \mathbb{B}{64} \ \mathtt{ECDSASIGN}(e \in \mathbb{B}{32}, p{\mathrm{r}} \in \mathbb{B}{32}) \equiv (v \in \mathbb{B}{1}, r \in \mathbb{B}{32}, s \in \mathbb{B}{32}) \ \mathtt{ECDSARECOVER}(e \in \mathbb{B}{32}, v \in \mathbb{B}{1}, r \in \mathbb{B}{32}, s \in \mathbb{B}{32}) \equiv p{\mathrm{u}} \in \mathbb{B}{64} $$

其中$p_u$为公钥,长度为64bytes的字节数组(由两个正整数$<2^{256}$连接而成),$p_r$为私钥,长度为32bytes的字节数组(或上述范围内的单个正整数),$e$为交易的哈希值。假设$v$是 "恢复标识符"。恢复标识符是一个长度为1byte的值,用于指定$r$为x值的曲线点的坐标的奇偶性和有限性;这个值的范围是$[27, 30]$,值27代表偶数$y$值,28代表奇数$y$值。

当且仅当r,s,v同时满足如下三个条件时,认为其有效:

$$ 0 < {r} < \mathtt{secp256k1n} \ 0 < {s} < \mathtt{secp256k1n} \div 2 + 1 \ {v} \in {27,28}\ $$

$$ \mathtt{secp256k1n} = 115792089237316195423570985008687907852837564279074904382605163141518161494337\ \mathtt{secp256k1n} =2^{256} - 2^{32} - 977 $$

对于一个给定的私钥,$P_r$,它所对应的以太坊地址$A(p_r)$(一个160位的值)被定义为相应ECDSA公钥的Keccak哈希值的最右边160位。即:

$$ A(p{\mathrm{r}}) = \mathcal{B}{96..255}\big(\mathtt{KEC}\big( \mathtt{ECDSAPUBKEY}(p{\mathrm{r}}))\ A(p{\mathrm{r}}) =\mathtt{KECCAK(P_u)[-159:-1]} $$

要签署的交易哈希值,$h(T)$,是交易的Keccak哈希值。有两种不同风格的签名方案。一种是在没有后三个签名元素的情况下操作,正式描述为$T_r$、$T_s$和$T_w$。另一个则对九个元素进行hash。 可以参考EIP-155

$$ L{\mathrm{S}}(T) \equiv \begin{cases} (T{\mathrm{n}}, T{\mathrm{p}}, T{\mathrm{g}}, T{\mathrm{t}}, T{\mathrm{v}}, \mathbf{p}) \space\space\space\space\space\space\space\space\space\space\space\space\space\space\space\space\space\space\space\text{if} \; v \in {27, 28} \ (T{\mathrm{n}}, T{\mathrm{p}}, T{\mathrm{g}}, T{\mathrm{t}}, T{\mathrm{v}}, \mathbf{p}, \beta, (), ()) \space\space\space\space\space\space\space\space\space\space\space\space\space\space\space\space\space\space\space\text{otherwise} \ \end{cases} \ where\space\space\space\space\space\space\space\space\space\space\space\space\space\space\space\space\space\space\space\space\space\space\space\space\space\space\space\space\space\space\space\space\space\space\space\space\space\space\space\space\space\space\space\space\space\space\space\space\space\space\space\space\space\space\space\space\space\space\space\space\space\space\space\space\space\space\space\space\space\space\space\space\space\space\space\space\space\space\space\space\space\space\space\space\space\space\space\space\space\space\space\space\space\space\space\ \mathbf{p} \equiv \begin{cases} T{\mathbf{i}}\space\space\space\space\space\space\space\space\space\space\space\space\space\space\space\space\space\space\space\text{if}\ T{\mathrm{t}} = 0 \ T{\mathbf{d}}\space\space\space\space\space\space\space\space\space\space\space\space\space\space\space\space\space\space\space \text{otherwise} \end{cases} \ h(T) \equiv \mathtt{KEC}( L_{\mathrm{S}}(T) ) $$

结合EIP-155得知:

六个参数分别是:(Nonce, gasprice, startgas, to, value, data), 其中V值的取值范围为[27, 28]

九个参数分别是:(Nonce, gasprice, startgas, to, value, data, chainid, 0, 0), 其中V值的取值范围变更为 $[{0,1}]+chainid * 2 + 35$。

chainid的取值表如下:

CHAIN_ID 公有链名称
1 以太坊主链
2 Morden链
3 Ropsten 以太坊测试链
4 Rinkeby 以太坊测试链
5 Goerli 以太坊测试链
1337 Geth 以太坊私有链

如下示例中,一笔交易的参数如下:

nonce = 9, gasprice = 20 * 10**9, startgas = 21000, to = 0x3535353535353535353535353535353535353535, value = 10**18, data='', chainid=1, 0, 0

整理为RLP的list为[9,0x4a817c800,0x5208,0x3535353535353535353535353535353535353535,0xDE0B6B3A7640000,,1,0,0]

则RLP编码为:

192+44=0xEC
09
8504a817c800
825208
943535353535353535353535353535353535353535
880DE0B6B3A7640000
80
01
80
80

则经过RLP编码后的数据$L_{\mathrm{S}}(T)$为:0xec098504a817c800825208943535353535353535353535353535353535353535880de0b6b3a764000080018080

:question:最后的80018080是怎么编码出来的呢?为什么不是80100 =>["",1,0,0] :heavy_check_mark:应该编码为0x80018080

:question:以及0x4a817c800的长度不足5bytes, 最后编码在前面补0?:heavy_check_mark: 其实质是值,而不是bytes,故在左侧补零。类似于9,补零成09,占据一个byte

:question:空字符串被编码为80 :heavy_check_mark: 空字符串和数值0被编码为0x80

则需要签署的交易hash值为:$h(T)$为keccak256(0xec098504a817c800825208943535353535353535353535353535353535353535880de0b6b3a764000080018080)=daf5a779ae972f972197303d7b574746c7ef83eadac0f2791ad23db92e4c8e53

则签名好的交易定义为:$G(T, p_{\mathrm{r}})$

$$ G(T, p{\mathrm{r}}) \equiv T \quad \text{except:} \ (T{\mathrm{w}}, T{\mathrm{r}}, T{\mathrm{s}}) = \mathtt{ECDSASIGN}(h(T), p_{\mathrm{r}}) $$

结合上文,即通过ECDSASIGN函数,将交易hash值和私钥作为参数传入该函数,得到三个返回值,依次是v,r,s.

Tw是恢复标识符或者是 $[{0,1}]+chainid 2 + 35$。 在第二种情况下,v​是链标识符chainid2+[0,1]+35,值35和36通过指定$y$的奇偶性来承担`恢复标识符'的角色,值35代表偶数值,36代表奇数值。

当使用私钥对交易哈希签名后,得到v,r,s三个值,然后可以通过ECDSARECOVER函数,分别以交易哈希,v,r,s为参数值,其返回值的最右侧160位就应该是该私钥对应的地址值。可以通过此方法来验证是否位该地址发出的真实签名

$$ S(T) \equiv \mathcal{B}_{96..255}\big(\mathtt{KEC}\big( \mathtt{ECDSARECOVER}(h(T), v0, T{\mathrm{r}}, T_{\mathrm{s}}) \big) \big) \ v0 \equiv \begin{cases} T{\mathrm{w}} \space\space\space\space\space\space\space\space\space\space\space\space\space\space\space\space\space\space\space\space\space\space\space\space\space\space\text{if}\ T{\mathrm{w}}\in{27, 28} \ 28 - (T{\mathrm{w}} \bmod 2) \space\space\space\space\space\space\space\space\space\space\space\space\space\text{otherwise} \end{cases} $$

$$ \forall T: \forall p{\mathrm{r}}: S(G(T, p{\mathrm{r}})) \equiv A(p_{\mathrm{r}}) $$

回到Stage1合约中,我们发现地址:0x7E5F4552091A69125d5DfCb7b8C2659029395Bdf, 是一个非常出名的地址,因为其私钥为:0x0000000000000000000000000000000000000000000000000000000000000001, 故针对题意:交易Hash值为keccak("stage1")=0xb6619a2d9d36a2acecba8e9d99c8444477624a46561077a675900f4af2c42c95, 故使用eth-sign方法可以得到v,r,s,注意在一般的web3.js中,在eth-sign时,会在签名的消息前加上\x19Ethereum Signed Message + len(msg)一段bytecode。故需使用不加入此消息的eth-sign函数库。

r = 0x274d91564d07600e8076a8843bd13a374cf43dcd2f5277fb61313f3d5c805b61
s = 0xa129687de0b602825f931363235f7a427088014fb94cde3264efbce58cc04236
v = 0x1c //28

上述值满足,ECDSARECOVER(keccak256("stage1"),0x1c,r,s) == Pu(私钥对应的地址)

bytes memory data = abi.encode(
        Entrypoint.solve.selector,
        (uint(bytes4(blockhash(block.number - 1))) &lt;&lt; 224) | 0xff1c, // => bytes4 guess
        0x274d91564d07600e8076a8843bd13a374cf43dcd2f5277fb61313f3d5c805b61,
        0xa129687de0b602825f931363235f7a427088014fb94cde3264efbce58cc04236
)

:four:Stage2 合约

function solve(uint16 a, uint16 b) public _ {
    require(a > 0 && b > 0 && a + b &lt; a, "something doesn't add up");
}

在进入到Stage2合约之前,我们需要先整理下data数据。注意到Entrypoint中,参数是bytes4,bytes4是从bytes32最左侧取4位byte作为值,而stage1的第一位参数是uint16, 其是从最右侧取2位值,并转换成uint。须注意的是:移位运算符只针对uint有效,对于Bytes类型不适用。故写作:

(uint(bytes4(blockhash(block.number - 1))) &lt;&lt; 224) | 0xff1c

在remix中,bytes32和uint256实质是同样的二进制代码,但是uint256得到值是数字表示,可以用于运算。

我们在看下:uint16 a和uint 16 b,我们知道uint16是从bytes32的右侧取2byte的值,故这里:a=0xff1c, b=0x5b61, 则a+b=0x5A7D, 满足条件

调用stage2的calldata为:

0x07e13e4d000000000000000000000000000000000000000000000000000000000000ff1c0000000000000000000000000000000000000000000000000000000000005b61

:five:Stage3 合约

function solve(uint idx, uint[4] memory keys, uint[4] memory lock) public _ {
    require(keys[idx % 4] == lock[idx % 4], "key did not fit lock");

    for (uint i = 0; i &lt; keys.length - 1; i++) {
        require(keys[i] &lt; keys[i + 1], "out of order");
    }

    for (uint j = 0; j &lt; keys.length; j++) {
        require((keys[j] - lock[j]) % 2 == 0, "this is a bit odd");
    }
}

注意:

要求1:idx需要对4取模,虽然idx是uint256,但实际上并无关系,只需要其uint8(idx)对4取模即可。此时的 idx % 4 = 0xff1c % 4 = 0, 故需保证keys[0]==lock[0]

要求2:要求保证keys中的4个值保持一个递增的关系。现在keys[0]=r, keys[1]=s已经确定且保持递增

要求3:要求对应位置的差值必须是偶数,两个偶数相减肯定是偶数,两个奇数相减肯定也是偶数。故这里我们保证两个值都是偶数即可。

此时的calldata应该为:

0x3f30497e
000000000000000000000000000000000000000000000000000000000000ff1c //idx
274d91564d07600e8076a8843bd13a374cf43dcd2f5277fb61313f3d5c805b61 //r key[0]
a129687de0b602825f931363235f7a427088014fb94cde3264efbce58cc04236 //s key[1]
x
y
274d91564d07600e8076a8843bd13a374cf43dcd2f5277fb61313f3d5c805b61 //lock[0]
0
0
0

:six:Stage4 合约

function solve(bytes32[6] choices, uint choice) public _ {
    require(choices[choice % 6] == keccak256(abi.encodePacked("choose")), "wrong choice!");
}

这题的考点主要是abi编码方式,对于不可变长度类型的定长数组的编码方式就是每32bytes依次排列。

可以看到key[2]或者key[3]都可以放置keccak256(abi.encodepacked("choose"))=e201a979a73f6a2947c212ebbed36f5d85b35629db25dfd9441d562a1c6ca896,故可以放置在key[2], 也可以放置在key[3]只要满足要求就行。由于要满足差值为偶数,故只能放置在key[3]中

0x3f30497e
000000000000000000000000000000000000000000000000000000000000ff1c //idx
274d91564d07600e8076a8843bd13a374cf43dcd2f5277fb61313f3d5c805b61 //r key[0]
a129687de0b602825f931363235f7a427088014fb94cde3264efbce58cc04236 //s key[1]
x //key[2]
e201a979a73f6a2947c212ebbed36f5d85b35629db25dfd9441d562a1c6ca896 //keccak(choose)
274d91564d07600e8076a8843bd13a374cf43dcd2f5277fb61313f3d5c805b61 //lock[0]
4
0
0

:seven:Stage5 合约

function solve() public _ {
    require(msg.data.length &lt; 256, "a little too long");
}

要求整个data的长度不能超过256

pragma solidity 0.4.24;
import "./setup.sol";
contract Hack {
    Entrypoint public entrypoint;
    constructor(address _setup) public {
        entrypoint = Setup(_setup).entrypoint();
    }
    function exploit() public {
        bytes memory data = abi.encodePacked(
            entrypoint.solve.selector,
            uint(uint16(0xff1c)|(uint(byte4(blockhash(block.number-1))) &lt;&lt; 224)),
            bytes32(0x274d91564d07600e8076a8843bd13a374cf43dcd2f5277fb61313f3d5c805b61),
            bytes32(0xa129687de0b602825f931363235f7a427088014fb94cde3264efbce58cc04236),
            bytes32(0xa129687de0b602825f931363235f7a427088014fb94cde3264efbce58cc04238),
            bytes32(keccak256('choose')),
            bytes32(0x274d91564d07600e8076a8843bd13a374cf43dcd2f5277fb61313f3d5c805b61),
            bytes32(0x0000000000000000000000000000000000000000000000000000000000000004)
        );
        uint size = data.length;
        address entry = address(entrypoint);
        assembly{
            switch call(gas(),entry,0,add(data,0x20),size,0,0)
            case 0 {
                   returndatacopy(0x00,0x00,returndatasize())
                   revert(0, returndatasize()) 
            }
        }
    }
}

注意点1:abi.encode('choose')和'choose'的编码并不一致。

'choose' => '0x63686f6f7365'
abi.encode('choose') => 是个动态类型string,先编码head,在编码tail。
0x0000000000000000000000000000000000000000000000000000000000000020 //head offset
  0000000000000000000000000000000000000000000000000000000000000006 //tail[0] length
  63686f6f73650000000000000000000000000000000000000000000000000000 //choose

注意点2:data是指向内存的指针,但是具体的数据指针是add(data, 0x20)位置处。

注意点3:assembly只能访问函数内部的局部变量,访问全局变量要使用sload(entrypoint.slot)来取值

点赞 2
收藏 0
分享
本文参与登链社区写作激励计划 ,好文好收益,欢迎正在阅读的你也加入。

5 条评论

请先 登录 后评论
bixia1994
bixia1994
0x92Fb...C666
learn to code