这是Paradigm CTF的一道题,:laughing:这道题考察的知识点比较多,涉及到ABI编码,RLP编码,黄皮书中附录F等。可以看作是一个编码相关的大集合题目。
这是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。下面我们分析下目标合约:
该合约是后续合约的母合约,一共干了三件事: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())
}
}
}
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
)
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))) << 224) | 0xff1c, // => bytes4 guess
0x274d91564d07600e8076a8843bd13a374cf43dcd2f5277fb61313f3d5c805b61,
0xa129687de0b602825f931363235f7a427088014fb94cde3264efbce58cc04236
)
function solve(uint16 a, uint16 b) public _ {
require(a > 0 && b > 0 && a + b < 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))) << 224) | 0xff1c
在remix中,bytes32和uint256实质是同样的二进制代码,但是uint256得到值是数字表示,可以用于运算。
我们在看下:uint16 a和uint 16 b
,我们知道uint16是从bytes32的右侧取2byte的值,故这里:a=0xff1c, b=0x5b61, 则a+b=0x5A7D
, 满足条件
调用stage2的calldata为:
0x07e13e4d000000000000000000000000000000000000000000000000000000000000ff1c0000000000000000000000000000000000000000000000000000000000005b61
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 < keys.length - 1; i++) {
require(keys[i] < keys[i + 1], "out of order");
}
for (uint j = 0; j < 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
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
function solve() public _ {
require(msg.data.length < 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))) << 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)
来取值
如果觉得我的文章对您有用,请随意打赏。你的支持将鼓励我继续创作!