NG#10 - 无序多签账户

关于合约账户有 3 个子任务，共同组成了 bad account 系列。本文是系列的第三个任务 Orderless Hashing。

分析

这题和上一题一样有 2 个合约，账户合约 GrandPharaoh 的构造器中指定了特定的公钥。__validate__中的get_multicall_hash会对每个 Call 递归计算 hash，最后用 异或操作 (^) 组成一个 hash，hash 是个 u256，low * high 是最终的 messageHash。我们构造传入签名的 r 和 s 能被 ECDSA 验证。

需要执行的 Calls 是另一个合约的 RoyalSpear.equip (0)，限制了只能被账户合约 GrandPharaoh 调用。

由于没有私钥，需要找到 ECDSA 中的漏洞去破解。

过程

首先需要看看 Calls 列表计算的无序多签最终是怎样的，可以是 [equip(0)]，是[equip(1),equip(0)]或者更多，只需要最后一个值是 equip (0) 就行。

这一部分可以用 snforge 写 cairo 的 test，在账户合约中添加(multicall_hash.low.into() * multicall_hash.high.into()).print();读取。

很明显能发现，[equip(0),equip(0)]对应的 hash 是 0，可以从此入手。

接下里深入 check_ecdsa_signature 中，看看具体是怎么进行签名验证的。

注释掉use ecdsa::check_ecdsa_signature;，把源码粘贴到 test 中，方便在必要的地方添加 print。

首先注意到是 let zG: EcPoint = gen_point.mul(message_hash);，由于 message_hash 是 0（无穷点），椭圆曲线中 P*0=0，所以 zG 是 0。如果用以下代码检查，会输出 'zG_x not exist'，因为无穷点是没有 x 坐标的。

Copy
match zG.try_into() {
Option::Some(pt) => {
let (x, _) = ec::ec_point_unwrap(pt);
'zG_x'.print();
x.print();
},
Option::None => { 'zG_x not exist'.print(); },
};

注释中写清楚了验证的公式是 (zG +/- rQ).x = sR.x，对应 zG + rQ 的代码是：

Copy
match (zG + rQ).try_into() {
Option::Some(pt) => {
let (x, _) = ec::ec_point_unwrap(pt);
if (x == sR_x) {
return true;
}
},
Option::None => {},
};

其中 zG 是 0，所以需要 rQ.x = sR.x，r 是传入的 sigature.r，Q 是公钥对应曲线上的点，s 是传入的sigature.r，R 是 r 对应曲线上的点。

很明显只需要 r=s 且 Q=R 就可以通过验证。其中公钥可以在区块链浏览器的部署交易中找到。

最后和上题同样方法使用 Starknet.js 中的 invokeFunction 传入特定 signature 和 calls 完成任务。

总结

如果深入学习了 ECDSA，知道椭圆曲线的运算规则，这题是很容易找到答案的。合约的多签设计太简单，可以利用签名重放进行攻击。

就此 Account 部分的三题就全部完成了。整个 Cairo 系列还有一题用 Cairo 写虚拟机的不打算做了，我讨厌算法题。未来会更新在 Starknet 实际开发中遇到的有趣的东西，敬请期待。