3

隐藏在众目睽睽之下:复盘 etherscan 的零日漏洞

 2 years ago
source link: https://www.chainnews.com/articles/693272340702.htm
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
neoserver,ios ssh client

隐藏在众目睽睽之下:复盘 etherscan 的零日漏洞

“如果你在 etherscan 中找到一个 0day (零日)漏洞,你会怎么做?我决定构建一个‘弹球机‘。" ——paradigm 研究合伙人 samczsun 注:原文作者是 samczsun。我喜欢挑战假设。我喜欢尝试做不可能的事情,找到别人错过的东西,用…

· 4 小时前 ·阅读约 8 分钟

隐藏在众目睽睽之下:复盘 etherscan 的零日漏洞

“如果你在 etherscan 中找到一个 0day (零日)漏洞,你会怎么做?我决定构建一个‘弹球机‘。" ——paradigm 研究合伙人 samczsun

隐藏在众目睽睽之下:复盘 etherscan 的零日漏洞

注:原文作者是 samczsun。

我喜欢挑战假设。

我喜欢尝试做不可能的事情,找到别人错过的东西,用他们从未见过的事来使他们震惊。去年的时候,我根据一个非常费解的 Solidity 漏洞为 Paradigm CTF 2021 编写了一个挑战。虽然公开披露了一种变体,但我找到的漏洞从未真正被讨论过。结果,几乎所有尝试过挑战它的人,都被它看似不可能的性质所难倒。

几周前,我们正在讨论关于 Paradigm CTF 2022 的计划,当时 Georgios 在推特上发布了一条难题推文,我认为在启动电话会议的同一天发布一个挑战会非常酷。然而,这不可能只是一次老掉牙的挑战。我想从这个世界得到一些东西,一些没有人会看到的东西,一些超越人们想象极限的东西。我想编写第一个利用 0day (零日)漏洞的以太坊 CTF 挑战。

隐藏在众目睽睽之下:复盘 etherscan 的零日漏洞

1

如何制造 0day (零日)漏洞

作为安全研究人员,为了优化我们的时间,我们会做出一些基本假设。一是我们正在阅读的源代码确实产生了我们正在分析的合约。当然,只有当我们从可信的地方读取源代码时,这种假设才成立,比如说 Etherscan。因此,如果我能找到一种方法让 Etherscan 错误地验证某些东西,我就能够围绕它设计出一个真正迂回的谜题。

为了找出如何利用 Etherscan 的合约验证系统,我必须验证一些合约。我在 Ropsten 测试网部署了一些合约来折腾并尝试验证它们。很快,我就看到了下面的界面。

隐藏在众目睽睽之下:复盘 etherscan 的零日漏洞

我选择了正确的设置并进入下一页。在这里,我被要求提供我的合约源代码。

隐藏在众目睽睽之下:复盘 etherscan 的零日漏洞

我输入了源代码并单击了验证按钮,果然,我的源代码现在附在了我的合约上。

隐藏在众目睽睽之下:复盘 etherscan 的零日漏洞

既然我知道了事情是如何运作的,我就可以开始戏弄验证过程了。我尝试的第一件事是部署一个新的合约,将 foo 更改为 bar,并用原始源代码验证该合约。毫不奇怪的是,Etherscan 拒绝验证我的合约。

隐藏在众目睽睽之下:复盘 etherscan 的零日漏洞

然而,当我手动比较两个字节码输出时,我注意到一些奇怪的事情。合约字节码应该是十六进制的,但显然有一些并非是十六进制的。

隐藏在众目睽睽之下:复盘 etherscan 的零日漏洞

我知道 Solidity 会将合约元数据附加到部署的字节码中,但我从未真正考虑过它是如何影响合约验证的。显然,Etherscan 正在扫描元数据的字节码,然后用一个标记替换它,“这个区域中的任何东西都可以不同,我们仍然会认为它是相同的字节码。”

对于潜在的 0day (零日)漏洞,这似乎是一个很有希望的线索。如果我可以欺骗 Etherscan 将非元数据解释为元数据,那么我将能够在标记为 {ipfs} 的区域中调整我部署的字节码,同时仍然让它验证为合法的字节码。

我能想到的在创建事务中包含一些任意字节码的最简单方法,是将它们编码为构造函数参数。Solidity 通过将 ABI 编码形式直接附加到创建交易数据上来对构造函数参数进行编码。

隐藏在众目睽睽之下:复盘 etherscan 的零日漏洞

然而,Etherscan 太聪明了,它将构造函数参数排除在任何类型的元数据嗅探之外。你可以看到构造函数参数是斜体的,以表明它们与代码本身是分开的。

隐藏在众目睽睽之下:复盘 etherscan 的零日漏洞

这意味着我需要以某种方式欺骗 Solidity 编译器,使其发出我控制的字节序列,以便使其类似于嵌入的元数据。然而,这似乎是一个很难去解决的问题,因为如果没有一些严重的编译器争论,我几乎无法控制 Solidity 选择使用的操作码或字节,之后源代码看起来会非常可疑。

我考虑了一会儿这个问题,直到我意识到:导致 Solidity 发出(几乎)任意字节实际上是非常容易的。以下代码将导致 Solidity 发出 32 个字节的 0xAA。

bytes32 value = 0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa;

受到鼓舞,我很快写了一个小合约,它将推送一系列常量,以便 Solidity 会发出与嵌入的元数据完全相似的字节码。

隐藏在众目睽睽之下:复盘 etherscan 的零日漏洞

令我高兴的是,Etherscan 在我的合约中间标记了 IPFS 哈希的存在。

隐藏在众目睽睽之下:复盘 etherscan 的零日漏洞

我快速复制了预期的字节码,并用一些随机字节替换了 IPFS 哈希,然后部署了生成的合约。果然,Etherscan 像往常一样考虑了不同的字节业务,并允许我的合约得到验证。

隐藏在众目睽睽之下:复盘 etherscan 的零日漏洞

有了这个合约,源代码建议在调用 example() 时应该返回一个简单的字节对象。但是,如果你真的尝试调用它,就会发生这种情况。

$ seth call 0x3cd2138cabfb03c8ed9687561a0ef5c9a153923f 'example()'
seth-rpc: {"id":1,"jsonrpc":"2.0","method":"eth_call","params":[{"data":"0x54353f2f","to":"0x3CD2138CAbfB03c8eD9687561a0ef5C9a153923f"},"latest"]}
seth-rpc: error: code -32000
seth-rpc: error: message stack underflow (5 <=> 16)

我在 Etherscan 中成功发现了一个 0day (零日)漏洞,现在我可以验证行为与源代码建议完全不同的合约。而现在,我只需要围绕它设计一个解密游戏。

2

一个错误的开始

显然,这个解密游戏将围绕这样一个想法,即 Etherscan 上看到的源代码并不意味着合约的实际行为方式。我还想确保玩家不能简单地直接重放交易,因此解决方案必须是每个地址唯一的。最好的方法显然是要求签名。

但是在什么情况下会要求玩家签署一些数据呢?我的第一个设计是一个具有单一公开函数的简单 puzzle。玩家将使用一些输入调用该函数,对数据进行签名以证明他们提出了解决方案,如果输入通过了所有各种检查,那么他们将被标记为 solver。然而,当我在接下来的几个小时内充实这个设计时,我很快就对事情的结果感到不满意了,它开始变得非常笨重和不优雅,我无法忍受在一个设计如此糟糕的 puzzle 上毁掉如此棒的 0day (零日)漏洞想法。

不得不承认,我无法在周五前完成这件事,于是我决定睡一觉。

3

Pinball 弹球难题

周末我继续尝试迭代我的初始设计,但没有取得更多的进展。就好像我现在的方法碰壁了,尽管我不想承认,但我知道如果我想要我满意的东西,我可能不得不重新开始。

最终,我发现自己从第一原理重新审视了这个问题。我想要的是一个解密游戏,玩家必须在其中完成各种知识检查。但是,我没有要求完成知识检查本身就是获胜的条件,相反,这可能是允许玩家选择的众多路径之一。也许玩家可以在整个 puzzle 中累积分数,利用这个漏洞可以获得某种奖励。赢的条件只是最高的分数,因此间接鼓励使用漏洞。

我回想起我去年设计的一个挑战,Lockbox‌,它迫使玩家构建一个单一的数据块,以满足六个不同合约的要求。合约会对相同的字节应用不同的约束,迫使玩家在构建有效载荷的方式上变得聪明。我意识到我想在这里做一些类似的事情,我会要求玩家提交一个单一的数据 blob,我会根据满足特定要求的某些数据部分来奖励分数。

正是在这一点上,我意识到我基本上是在描述 pinboooll‌,这是我在 DEFCON CTF 2020 决赛期间面临的一个挑战。pinboooll 的噱头是当你执行二进制文件时,执行会在控制流图上反弹,就像一个球在弹球机中反弹一样。通过正确构造输入,你将能够找到特定的代码部分并获得分数。当然,也有一个漏洞,但坦率地说,我已经忘记了它是什么,我也不打算再去寻找它。除此之外,我已经有了自己想要利用的漏洞。

由于我在处理的是一个运行当中的零日漏洞,我决定要尽快解决这个难题。最后,我花了几个小时让自己重新认识 pinboooll 的工作原理,并花了几天时间将其重新实现。这就解决了 puzzle 的支架问题,现在我只需要集成这个漏洞。

4

武装化一个零日漏洞

我让 Solidity 输出正确字节的方法一直是只加载几个常量,并让 Solidity 发出相应的 PUSH 指令。然而,这样的任意常数可能是一个巨大的危险信号,我想要一些能更好地融入其中的东西。我还必须加载一行中的所有常量,这在实际代码中很难解释。

因为我真的只需要硬编码两个神奇的字节序列(0xa264...1220 和 0x6473...0033),所以我决定看看是否可以在它们之间夹入代码,而不是第三个常量。在已部署的合约中,我只需要用一些其他指令替换夹在中间的代码。

address a = 0xa264...1220;
uint x = 1 + 1 + 1 + ... + 1;
address b = 0x6473...0033;

经过一些实验,我发现这是可能的,但前提是启用了优化器。否则,Solidity 会发出过多的值清理代码。这是可以接受的,所以我继续改进代码本身。

我只能在两个地址内修改代码,但是在最后看到一个悬空的地址会很奇怪,所以我决定在条件语句中使用它们。我还必须证明第二个条件的必要性,所以最后我投了一点分数奖励。我做了第一次有条件的检查,检查 tx.origin 是否匹配了硬编码的值,以给人们一个最初的印象,即没有必要进一步追求这个代码路径。

if (tx.origin != 0x13378bd7CacfCAb2909Fa2646970667358221220) return true;
state.rand = 0x40;
state.location = 0x60;
if (msg.sender != 0x64736F6c6343A0FB380033c82951b4126BD95042) return true;
state.baseScore += 1500;

现在源代码都准备好了,我必须编写实际的后门。我的后门需要验证玩家是否正确触发了漏洞利用,如果他们没有正确触发,则不给出任何提示就失败,如果他们成功触发,则奖励他们。我想确保漏洞不会被轻易重放,所以我决定只要求玩家签署自己的地址,并在交易中提交签名。为了增加乐趣,我决定要求签名位于交易数据中的偏移量 0x44 处,球通常会从这里开始。这将需要玩家了解 ABI 编码的工作原理,并手动将球数据重新定位到其他地方。

但是,在这里我遇到了一个大问题:根本不可能将所有这些逻辑都放入 31 字节的手写汇编中。幸运的是,经过一番考虑,我意识到我还有另外 31 个字节可以使用。毕竟,真正嵌入的元数据包含了另一个 Etherscan 也会忽略的 IPFS 哈希。

在打了一些代码高尔夫之后,我抵达了一个可以工作的后门。在第一个 IPFS 哈希中,我会立即弹出刚刚推送的地址,然后跳转到第二个 IPFS 哈希。在那里,我会哈希调用方,并部分设置 memory/ 堆栈以调用 ecrecover。然后我会跳回第一个 IPFS 哈希,在那里我完成设置堆栈并执行调用。最后,我将分数乘数设置为等于 (msg.sender == ecrecover()) * 0x40 + 1,这意味着不需要额外的分支。

在把后门编码成一定大小后,我在推特上发布了我的 Rinkeby 地址,以便从水龙头处获取一些测试网 ETH,然后向任何观看这条推文的人暗示可能会发生一些事情。接着,我部署了合约并对其进行了验证。

隐藏在众目睽睽之下:复盘 etherscan 的零日漏洞

现在剩下要做的,就是等待有缘人发现隐藏在视线中的后门。

注:截至发稿时,来自 Rocket Pool 的软件开发者 Kane Wallmann 已经解出了这个谜题,具体过程在这里:https://medium.com/@kanewallmann_71759/an-untrustworthy-pinball-machine-d9dcd07882c

此外,Etherscan 开发者 Caleb Lau 也已经修复了该漏洞。

隐藏在众目睽睽之下:复盘 etherscan 的零日漏洞


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK