Appearance
挑战高楼!揭秘智能合约的“电梯难题”
你有没有想过,在数字世界的摩天大楼里,一部看起来平平无奇的智能合约“电梯”,竟然暗藏玄机,让你止步不前?今天,我们就来揭开这场名为“Elevator”的CTF挑战的面纱,看看如何巧妙地“乘坐”这部不听话的电梯,抵达顶层!
故事的开端:一部“不守承诺”的电梯
我们面前的“Elevator”合约,负责控制一栋大楼的电梯。它有两个重要的状态变量:top(布尔值,表示是否在顶层)和floor(无符号整数,表示当前楼层)。
它的核心功能是 goTo(uint256 _floor) 函数。乍一看,这似乎很简单,输入想去的楼层,电梯就带你去。然而,这里有一个关键的限制:
solidity
Building building = Building(msg.sender); // 电梯认为调用者就是“大楼”
if (!building.isLastFloor(_floor)) { // 只有当目标楼层不是最后一层时,才更新楼层
floor = _floor;
top = building.isLastFloor(floor);
}
问题就出在这里! goTo 函数在更新楼层之前,会调用一个名为 isLastFloor 的函数来判断目标楼层是否是“最后一层”。但它期望的调用者(msg.sender)是“大楼”(Building 接口的实现者)。这意味着,只有当电梯以为自己正在被“大楼”调用,并且“大楼”告诉它“你请求的楼层不是最后一层”时,它才会执行楼层更新。
智能的漏洞:Solidity 的“不完美承诺”
Solidity 的一个特性是,它在接口调用时,会尝试将调用者(msg.sender)强制转换为指定的接口类型。在这里,Elevator 合约直接将 msg.sender 转换成了 Building 接口。
想象一下,如果你自己扮演“大楼”的角色,并且有能力控制 isLastFloor 函数的返回值,会发生什么?
破解之道:反客为主,重塑逻辑
CTF 题目提供了一个绝妙的解决方案——Hack.sol 合约。这个合约的设计思路是:
- 模拟“大楼”:
Hack合约实现了Building接口,并提供了一个isLastFloor函数。 - 控制“最后一层”的判断:
Hack合约的isLastFloor函数非常狡猾。它有一个计数器ct。- 第一次被调用时(也就是
Elevator合约的goTo函数第一次检查isLastFloor时),ct为 1,isLastFloor返回false(因为ct > 1为false)。 - 第二次被调用时,
ct变为 2,isLastFloor返回true(因为ct > 1为true)。
- 第一次被调用时(也就是
- 诱导
Elevator:Hack合约的runGoTo函数调用target.goTo(9999999)。- 此时,
msg.sender是Hack合约。 Elevator合约将Hack合约的地址强制转换为Building接口,并调用Hack的isLastFloor函数。- 第一次调用
isLastFloor,返回false。Elevator内部逻辑判断!building.isLastFloor(_floor)为true,所以会执行floor = _floor;,将楼层设置为9999999。 - 紧接着,
Elevator合约会再次调用building.isLastFloor(floor)来更新top状态。这次,Hack合约的isLastFloor函数会被第二次调用,ct已经大于 1,所以返回true! Elevator合约最终将top设置为true。
- 此时,
至此,我们成功地“欺骗”了 Elevator 合约,让它认为自己已经达到了顶层,即使我们并没有真正访问一个“最后一层”的逻辑。
测试验证:代码中的“电梯运行记录”
98_test_elevator.ts 文件展示了如何用代码来验证这个攻击。它首先部署了一个 Hack 合约,并传入了 Elevator 合约的地址。然后,调用 hack.runGoTo()。最终,它会检查 Elevator 合约的 top 状态是否被成功地修改为了 true,以及 floor 是否更新为 9999999。
结论:智能合约的安全,需要对细节的深刻理解
“Elevator”这个CTF题目,巧妙地利用了Solidity在接口调用和类型转换方面的一些特性。它告诉我们,在编写智能合约时,必须对 msg.sender 的行为、接口调用的预期以及逻辑的每一个细节都有清晰的认识。一个小小的“不守承诺”,就可能成为攻击者突破防线的关键。
所以,下次当你遇到一个看起来“死活不让你上去”的智能合约时,不妨想想,它是不是也藏着这样一个“不守承诺”的电梯呢?